I have argued in the past that extending classes in OOP is something we should avoid in favor of dependency injection; but this is actually not always the case. Recently I have come across a scenario where extends provide an elegant solution to a difficult problem.
Fundamentally, this subject touches on the problem of accessing other classes' properties and methods — something a lot of us with a procedural background has struggled with. I have now been using OOP for a few years in PHP, and I am still learning and improving my code.
I still think using dependency injection is the best option in the majority of cases, but there is a very specific situation that calls for using extends. I have not experimented much with singletons yet, but their use is also discouraged, since they hide dependencies of the classes using them.
Extending classes in OOP
We should probably never have a users class that extends a database class; instead we should pass the database class to the users class using DI. This approach is still the most maintainable and flexible.
The problem with extends is that they tend to limit the portability and maintainability of our code. If a database class is extended by a users class, we will not be able to easily re-use the users class in another context without also having to instantiate the database class.
This is actually a very big problem, since you will end up with very tightly coupled dependencies.
I wrote the first version of Beamtic using inheritance, but I quickly realized that I could not easily re-use my code outside of the "core" functionality. To solve that problem I actually spend quite some time, reading about OOP, and refactoring my code to using DI.
Fast forward a few years, and I find myself going back to using inheritance for specific features located in the "top" layer of my applications.
When to extend classes
There is at least one scenario when extending a class makes sense. Sometimes you will be working on parts of an application that are laser focused at solving specific problems, but still, these parts might have some underlying code in common, which is not really used outside of this specialized context; this may apply mostly to the top layers of an application.
One example would be a tagging system for a website that enables you to "categorize" different pieces of content using "tags". Such a system would also require methods to manage the tags on the site, let us say it has the following features:
- add tag
- delete tag
- remove tag from content
- show all tags
- show content tagged as
The features themselves does not really matter—it is just an example.
In many cases, you can handle all these features from within the same class with a few if statements, but as the project continues to grow, so will the time overhead in navigating your code. What can we do to solve that problem and make the project easier to manage? We can split the class into multiple classes, of course.
But, now another problem arises: the methods inside the individual classes depend on the same shared methods; how do we best solve this problem? Do we duplicate these shared methods in each individual class? Of course not!
An elegant solution is to create an abstract "base" class, and then write the different features (classes) as extensions of that class. The application can then instantiate the requested feature/action dynamically, without having to keep track of which extra dependencies to provide via DI—and, at the same time, we avoid the possibility of someone accidentally instantiating the specialized dependencies outside of the context in which they are intended to be used.
Inside the abstract class we can take care of defining properties and methods that are shared between our, other, specialized classes. If then, at some point, a method inside the abstract class becomes useful more broadly, moving it to a more suitable place will be piece of cake.
- Classes defined as "abstract" can not be instantiated on their own, which will help avoid developer confusion.
- An abstract class can also extend another abstract class.
- Methods defined as abstract must be implemented by the child class (the extending class).
- Protected properties and methods are only accessible within the parent and child classes.
- Private properties and methods can not be accessed by child classes.
We can include the "abstract" word in the beginning of the file name, in accordance with PSR naming conventions—thereby developers will instantly know that new parts (classes) added should extend the "base" class.
A suitable file name would be:
You may remove the underscores if you wish, I personally prefer them to camelCase for readability.
Subsequent child classes will then be named like this:
lib/tagging/add_tag.php lib/tagging/delete_tag.php lib/tagging/remove_tag_from_content.php lib/tagging/show_all_tags.php lib/tagging/show_content_tagged_as.php
While I still think we should avoid the use of inheritance in general, it seems there is indeed cases where extending classes can be the most logical solution—the top layers of an application is a good example—an example would be whatever "part" is loaded by a router.
Under the specific circumstances where it does make sense, I think using an abstract class more concisely communicates the intention of choosing inheritance to other developers; including a comment within the class itself can then clear up any remaining doubts as to how the class should be used.
Finally, this is probably not the only case where extends are useful; I only intended to demonstrate at least one scenario where inheritance can be useful.