Essay
Including A Constructor In Your Interface
Today, while looking through a pull request containing PHP code, I stumbled over an issue that is mostly non-existent in other languages, so it might be difficult to f...
Today, while looking through a pull request containing PHP code, I stumbled over an issue that is mostly non-existent in other languages, so it might be difficult to find reliable information on best practices regarding this specific topic. I was, of course, delighted to see that the code in question included an interface and its related standard implementation, instead of a simple class. However, the interface included a constructor as well.
When you try to find out whether you should include a constructor within interfaces, you’ll probably end up realizing that this is not even possible in most other languages. PHP is very permissive in what it allows you to put into interfaces, sometimes to the point of making you consider choices you should not even be able to consider, to begin with.
So, while you might already have guessed from that last statement that I’m opposed to the idea of having constructors in an interface, even though PHP might allow it, I want to try to explain the reasoning behind my stance.
Contract About Interaction Between Objects
Object-oriented programming is about objects, and how they communicate with each other. An interface is a contract that fixes the details about how specific objects want to be interacted with. Through the interface, an object lets you know what vocabulary it understands. So, whoever the other party is, as long as it uses the correct vocabulary, it is welcome!
I’d like to quote a very important passage by Alan Kay from a mailing list discussion he had after being disappointed in what people generally perceived to be the most important point of object-oriented programming:
Just a gentle reminder that I took some pains at the last OOPSLA to try to remind everyone that Smalltalk is not only NOT its syntax or the class library, it is not even about classes. I'm sorry that I long ago coined the term "objects" for this topic because it gets many people to focus on the lesser idea. The big idea is "messaging" -- that is what the kernal of Smalltalk/Squeak is all about [...]. The Japanese have a small word -- ma -- for "that which is in between" -- perhaps the nearest English equivalent is "interstitial". The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
So, the interface does only and exclusively care about the interaction points between objects. The instantiation (which is the constructor’s responsibility) is not about how objects interact, but rather about how to turn a class (blueprint) into an object in the first place. At the point where the constructor comes into play, you don’t even have an object yet.
Devoid Of Implementation Details
The constructor is used to put an object into an initialized state when it is instantiated. The current state of an object, and whether it is considered valid or not, is an implementation detail, and should therefore not be relevant for the interface.
The internal properties that define the state of an object should be inaccessible to outside objects. As the constructor should only deal with these internal properties, all of its work should therefore also be hidden from external objects as well.
If you get passed a View object, and you want to render that view, you should not need to know whether that specific object had also asked for a Logger when being instantiated.
As Lean As Possible
The Interface Segregation Principle (ISP), which is the I in SOLID, states that an interface should only contain the methods needed for one specific role. So, as an (extremely simplified) example, when creating an interface for a Validator object, the interface probably should only contain the single method validate() .
If the interface contains too many methods, this probably points to a design issue where your interface just mirrors your classes. This might lead to code where different responsibilities are so tightly coupled that you can’t make changes to one without affecting the other.
Consider Dependency Injection
When using dependency injection (and you really should), you usually pass in the dependencies of your object to be instantiated through its constructor. So, as an example, your console command might have an Input dependency and an Output dependency, so that it can receive input, parse it, and echo the corresponding output. This will yield a constructor along the lines of:
public function __construct(Input $input, Output $output);
Now, what if you now want to add logging to all of your console commands? You’ll need an additional dependency on a Logger as well. This will change your constructor, though. To keep the option of replacing one implementation with another one, you need to be able to adapt the constructor so that it can accept whatever dependencies you will need. And adapting the constructor should not break your existing code.
Consider Multiple Implements
If you do have your interfaces properly segregated, like described above, you might want to have a class that implements several interfaces at once. As an example, you might want to have something like this:
class CachedRenderer implements Cacheable, Renderable { }
In such a case, if both interfaces would include a constructor, they would dictate incompatible signatures for your class. You would not be able to write a constructor that would work with both, except in the rare case where their constructor is identical.
Instantiation Needs Implementations Details
The goal of using interfaces is to make your code agnostic of specific implementations. Whenever you want to call a method declared in an interface, you can just rely on the object implementing the interface to accept that method call.
Instantiation is different, though. You cannot instantiate a new object without knowing some implementation details about the specific class to use. At some point, whether it is directly in one of your classes or factories, or whether it is within an IoC container, you will need to write a statement along the lines of:
$object = new \ExactNamespace\ExactClassToInstantiate(
$arguments,
$thatThe,
$exactClass,
$needs
);
It is just not possible to instantiate a new object without knowing details about the implementation. That’s why the instantiation is usually put within factories and/or DI containers, you want to have one single place to make changes if you want to replace classes.
What About The Liskov Substitution Principle?
The Liskov Substitution Principle (LSP), which is the L in SOLID, states that wherever an object is used, you should be able to replace it with a subtype of that object without breaking the code. This means that your new object should be able to do everything that the original object could do or more.
So, what about the example we’ve given above where the subtype has a different constructor signature than the interface it implements. Won’t this break the LSP?
The LSP is not even concerned with instantiation at all. It does not state that you should be able to instantiate all classes in the same way. It only states that, when an object of a class is being used somewhere, it should be substitutable for an object of a subclass. Note the term “object” - we’re already dealing with instantiated objects at this point. The LSP is still intact, even if we have different constructors for different implementations.
What If I Want To Enforce A Specific Signature?
If you absolutely must provide a specific signature to be used as part of your API, you are already defining one part of the specific implementation, not just the contract.
In such a case, you should probably use an abstract class. Within the abstract class, you can provide as much implementation details as wanted or needed, while also marking up the parts that the extending class will need to take care of. So, you have a kind of implementation spectrum you can position your code on:
- Interface => no implementation specifics provided
- Abstract Class => some implementation specifics provided
- Class => all implementation specifics provided
Conclusion
Don’t put constructors into your interface definitions. They don’t serve a valid purpose, and they might cause all sorts of issues down the road.
Did I miss something obvious? Let me know in the comments below!