OO in D8: Inheritance vs. Aggregation (Part 3)
Object-Oriented Design in Drupal 8 Mini-Series
By Tobias Stöckler
It's been a while since I visited the Symfony Summit in Cologne in the beginning of July. I had two days of training there, one of which was on the topic of Object-Oriented Design. All other participants used Symfony on a daily basis, but none of them had worked with Drupal before, at least not in-depth. So, as a Drupal 8 core developer, I had a unique perspective on the presented topics. There were three things in particular that stood out to me in the training, and I'd like to share them with you as part of this mini-series. The first post was about Interfaces vs. Abstract Classes and the second post about Being Static vs. Being Called Static.
*/
For me class inheritance had always been one of the central aspects of object-orientation and utilizing it for code re-use one of the prime benefits of object-oriented code. The problem with inheritance is that in PHP a class can only sub-class a single other class. So if a third-party system provides a class, which I want to expose as an entity type in Drupal I cannot extend both the third-party class and the Drupal Entity
class.
A different problem, that got highlighted at the training, is that often times you do not need everything of the class you are extending. Keep in mind that a class inherits all functions and properties, be they public, private or protected. Especially when taking into account multiple levels of inheritance you can easily inherit things you don't actually need in your class. The same thing can happen when interfaces extend each other in multiple levels. Drupal's EntityInterface
, which the aforementioned Entity
class implements, is an example of this. It extends TypedDataInterface
forcing Entity
to implement methods (such as getName()
) which do not actually make sense for it. This is obviously far from ideal.
There are two ways to improve this situation:
-
Refactoring the base interfaces so that they are nice and small and do exactly one thing. Accordingly, these interfaces should probably only contain one or two methods. The trainer at the SymfonySummit noted that interfaces tell you something you can do with an object so the "able" suffix in the English language is a good hint for whether something is a good, precise interface. PHP's
Serializable
andCountable
interfaces are the boilerplate examples for this, but also Drupal'sIdentifiableInterface
. If you have your base interfaces cleanly separated you can have an interface such asEntityInterface
extend all the base interfaces it needs (such asIdentifiableInterface
) but you will no longer have any superfluous functions. There are already a number of great ongoing efforts in Drupal 8 that go in this direction, among them removingTypedDataInterface
and reducingEntityInterface
. -
Instead of achieving code re-use by inheritance one can also re-use code by aggregation. In the example of the third-party class that I want to expose as an entity, I can simply have the third-party class and Drupal's
Entity
class injected in my class and forward the appropriate calls to them where needed. I would forward theentityInfo()
function to Drupal'sEntity
class as$this->drupalEntity->entityInfo()
and forward anauthenticate()
function to the third-party class as$this->thirdParty->authenticate()
. This circumvents this problem completely: My class is still free to extend a different class if I want and is also not forced to implement any interfaces if I do not want it to. This pattern is not used very much in Drupal so far. The entity controllers, however, are a manifestation of this pattern. An entity type's class gets its storage controller injected, for example, and then calls that in itssave()
method.