There was much discussion on Twitter about the concepts of using “final” and “private” in objects, and what exactly the best practices are. The conversation seemed to boil down to three distinct questions:
- Should an object be open for extension, and expose its internals for that purpose?
- Does exposure of those internals create a de facto contract with children for their behavior?
- Should software only be used as intended by its designers, or should it be modified, extended and changed by the end user to fit certain, specific goals?
Should an object be open for extension, and expose its internals for that purpose?
Object extension and inheritance are certainly hot topics. And there are many conversations that have happened over the years surrounding the best practices of inheritance. Anyone that’s seen me speak knows that I preach against the extension of the interface – that is, extending an object and adding new public API methods is, in my opinion, bad form. PHP doesn’t practice any form of type safety, meaning that parents might pass the type hint, but fail to implement the methods needed for actual execution.
Marco Pivetta makes an excellent case in a blog post called when to declare classes final that classes should extend from a discrete, specific abstraction, and that concrete implementations should be final to prevent inheritance. He argues that this encourages composition while still preserving the ability of later developers to create substitute objects, by implementing the abstraction (the interface).
I see Marco’s point, and I think that it has merit. However, I disagree with explicitly marking classes as final in many cases, seeing that as overkill. PHP is special and unique in the fact that it expresses concepts like visibility and finality through specific language constructs; these concepts are expressed in other languages only through standards and conventions. That does not make PHP specifically better than, say, Python (because we have levels of visibility). Marco makes a case for never ever trusting anyone (including yourself) and enforcing the rules through available language constructs, which is fine. It’s something I disagree with.
What I do agree with is the concept that we should have our concrete objects extend from higher level abstractions like interfaces, or, if the use case exists, abstract classes. And I also agree that having a shallow inheritance tree is a useful and beneficial thing.
Where I run into problems is with the idea that objects should be completely closed off for extension, or that “copy and paste” is a superior method over inheritance. The reason for this is that other developers may make mistakes or may not fully understand my particular use case. There have been many times where overriding some method would have solved a problem, but instead I was forced to decorate an object simply because the developers designed their class with a private method instead of a protected one.
I favor leaving objects open for (limited) extension, and exposing internals for this reason. I especially favor this for frameworks and libraries. Extension, though, should NOT change the public API; doing so is reserved for the interface ONLY.
Does exposure of those internals create a de facto contract with children for their behavior?
The short answer is, NO.
The longer answer is a bit more complicated.
Objects know one another by their interfaces, which is to say their public methods. Object A knows Object B by what methods it can call on Object B. The internals of Object B are in no way a concern to Object A, insofar as Object B continues to return values that Object A expects.
But herein lies the rub: objects know each other through their interfaces, which are the contracts that are established between objects. When inheritance takes place, you have not created a contract between two objects; you’ve instead created a new single object. In fact, when you extend Object A with Object C, you have created a new whole object called Object C.
That doesn’t mean that children don’t rely on their parents’ behavior for certain functionality. They most certainly do. Changing the internals of a parent object in a library can have devastating effects if the changes no longer reflect what the children expect. But the open/closed principle was not created to address this specific use case. In fact, the open/closed principle originally encouraged active extension by banning the modification of an object once it was created and finalized. Though most developers now reject this interpretation (known as Meyer’s Open/Closed Principle) in favor of a more liberal principle (the Polymorphic Open/Closed Principle), that principle still teaches that the internals are open for modification, but the interface is not. In other words, there is no principle of SOLID that expects the internals to create a contract between parent and child classes.
Should software only be used as intended by its designers, or should it be modified, extended and changed by the end user to fit certain, specific goals?
At some point, the recent conversation diverged into an area of discussing whether or not software should be created and used for a specific purpose, and whether or not developers should then enforce that specific purpose through their development practices. The participants wondered whether or not intent mattered in the final outcome, and whether or not that intent could, or should, be enforced.
I found much of this part of this discussion antithetical to the concept of open source software design in general. Open source purports that software is designed for a specific purpose, but the use of that software is entirely up to the end user. Mozilla and Firefox surely couldn’t have foreseen the need for Tor. Yet I imagine that if the designers of Firefox had applied the principles sought by various members of the discussion, Tor would never have come into existence and the world would be worse off for it.
The only real solution here is either to permit extension of objects for use cases previously unforeseen, or to allow developers to craft their own objects by providing extensible interfaces. Either one solves this problem and allows for unpredictable outcomes. Inheritance isn’t the only way to solve this problem, but for developers who don’t believe in or use interfaces, they also have no business using final and private in their code.
Object-oriented software is by no means a settled practice
Despite the fact that we know a lot about how to design object-oriented applications, there’s also a lot we don’t know, or that’s still being hashed out. Our understandings change, and there’s definitely room for discussion and debate about these topics. Some topics exist as extensions of our deficiencies in languages or technology; others are fundamental disagreements about the very nature of object-oriented design practices.
In the end, no application is ever perfectly designed and all design decisions come with tradeoffs. The goal is not to be perfect, but to do good work with the available tools and concepts we know and understand.
Frustrated with your company’s development practices?
You don't have to be!
No matter what the issues are, they can be fixed. You can begin to shed light on these issues with my handy checklist.
Plus, I'll help you with strategies to approach the issues at the organization level and "punch above your weight."