Being one of the building blocks of the SOLID principles, the Liskov Substitution Principle is a fundamental pillar of Object Oriented Design (OOD), whose formulation is aimed at solving the issues associated with poorly-designed hierarchies of classes. Even though its formal definition is somewhat hard to grasp, in practical terms it states that methods defined in a base class (or interface) and their derivatives must have the same signature, preconditions should be weakened in the formers, and post-conditions should be strengthened. In addition, if methods in subtypes throw exceptions, they should be of the same type as the ones thrown by the parent abstraction.
It might take a while to understand the logic that stands behind LSP; possibly the most challenging facet is to demonstrate the consequences when its infringement occurs in the real world. So, in an attempt to cover the principle from this standpoint, in the introductory part of this tutorial, I showed how the improper implementation of a class hierarchy in a composite view rendering module can cause unexpected exceptions when one of the module’s subtypes (in this case a child class tasked with rendering partial views) unintentionally strengthened the aforementioned preconditions.
If you missed the first installment or would like a refresher, you can find the article here:
Defining a Few New Segregated Contracts
As I explained in the introductory chapter, my biggest mistake was to place (from the very beginning) the entire composite view module on the wrong abstraction. While any composite view could be easily consumed by my trivial layout builder, things were radically different when the builder was fed with a partial, as the process always threw an unexpected exception. This was a clear sign of code smell and a flagrant violation of LSP, so I decided to switch to a few granular and disparate contracts, instead of using the one declared by a single abstract class.
With that idea in mind, the first thing that I needed to do was to define the aforementioned contracts, something that in this particular case was achieved through some segregated interfaces, which looked like this:
Although the methods declared by the above interfaces were pretty self-explanatory, they solved my main issue: simply put, they gave me the liberty to create composite and partial views without having to rely on the same abstract parent. And - most importantly - without breaking up its contract in the subtypes.
Moreover, if I ever wanted to create a composite view class, the process would be reduced to spawning an implementer of the pertaining interfaces and nothing else. On the other hand, a partial would implement only the “renderable()” interface, as it wouldn’t need the functionality required for adding and removing other views.
With these highly-granular interfaces already set, I had a much clearer idea of how to implement an improved version of the composite view module. There was, however, an additional step that needed to be taken to get things working as intended: it was necessary to encapsulate common functionality shared by the composite views and partials in one single place. Thus, I thought to myself that the best way to accomplish this would be by creating an abstract view class; so after thinking for a while I ended up building a brand new base class along with a couple of derivatives.
To see how these classes were built, move forward and keep reading.
Taking the Next Step: Refactoring the Composite View Rendering Layer
The most effective way to create an LSP-compliant hierarchy of composite and partial views was to rest the whole structure on a new abstract parent, whose contract and functionality were shared by both types of views.
With that concept in mind, I came up with an improved base class, whose implementation was as follows:
Even though the revamped version of this abstract class looks fairly similar to the one implemented in my first take, it does expose a subtle difference worth pointing out: first and foremost, the class no longer declares the abstract “addView()”, “addViews()” and “removeView()” methods, something that allows the sharing of common functionality between composite and partial views and extends their contracts by means of the set of segregated interfaces defined before.
Naturally, the best way to understand how to deal with composites and partials is by showing the couple of subclasses that bring them to life. Thus, here are the refactored incarnations of them:
Now the implementations of the concrete “CompositeView” and “PartialView” classes look way better, as they only extend the functionality of their abstract parent, in order to handle composite views and partials in a pretty straightforward fashion. In short, this means that all the conditions stated by LSP are nicely met, and - as a consequence - it’s possible to substitute the base type with subtypes without breaking up a single segment of client code.
But, is this entirely true? Well, as long as the client code consumes instances of “AbstractView”, all should work like a charm. However, what would happen with my humble layout builder, which only accepts composite views to do its business? For obvious reasons, it’s necessary to amend the implementation of the builder too, so that it can work side by side with the revamped version of the “CompositeView” class.
Making a Final Change: Modifying the Implementation of the Layout Builder
As noted in the previous segment, the last change that I needed to introduce into my composite view module was to modify the original implementation of the layout builder. Since this client class initially used its “attachView()” method to take instances of the “AbstractView” class and generate layouts, it was necessary to amend the signature of the method in question, so that it could consume only composite views.
After making this final change, the definition of the builder was as follows:
There you have it. At this point, the improved layout builder looked much more effective than the initial one, as the mentioned “attachView()” method was modified to take only composite views. This subtle –yet relevant – detail assured that any object injected into the builder’s internal would implement the “Aggregable” interface (and thereby its “add()” and “addMultiple()” methods) and wouldn’t throw any unexpected exceptions.
Naturally, the best manner to prove that the functionality of the builder was the intended one was by example. Thus, I set up a simple script that put the builder into action (again, the autoloader was omitted for the sake of clarity). Take a look at it:
Mission accomplished. Unlike my first attempt to test the composite view module, which made me confront an unpleasant exception, this time the script ran smoothly and effectively didn’t raise anything weird or unexpected. In summary, I finally managed to create a hierarchy of classes and interfaces that nicely followed the requirements stated by LSP.
From the previous examples, it’s clear to see the strong relationship that there exists between LSP and design by contract. No matter if contracts are set in interfaces or abstract classes, they shouldn’t never be wildly overridden by subtypes, unless this is absolutely necessary (which is a clear sign of a bad design). So, keep in mind LSP’s statements and your life and your clients’ will be much happier.
Over the course of this two-part tutorial, I provided you with a humble introduction to the concepts that surround the application of the Liskov Substitution Principle in PHP, and how its unintentional violation in a real world situation can cause a lot more of headaches than one might think at first. Even when this infringement was, at least in my personal case, relatively harmless in general, the implementation of an effective solution required to refactor several chunks of my original composite view rendering module, not to mention the fact that I had to switch over a different abstraction to make the layout builder to consume only the type of objects that it expected to receive.
Was this a harsh lesson to learn? Undeniable, it was. However, in the end it made me realize that dealing with an incorrect hierarchy of classes where a ISA relationship isn’t entirely fulfilled is an issue that should never be left floating in the limbo.
What’s more, even when PHP exposes a somewhat immature object model (especially when compared to older players like Java and C++), which doesn’t support covariance and contravariance, and where you can implement pre/post conditions in a quite brittle way, the use of LSP has a lot of sense, trust me. Thus, make sure you stick to it in every possible use case, as your OOP applications will rest on a more solid and easier to maintain structure.
See you in the next PHP development tutorial!
blog comments powered by Disqus