The Single Responsibility (SRP), Open/Closed (OCP), Liskov Substitution, Interface Segregation, and Dependency Inversion. Five agile principles that should guide you every time you write code.
Because both the Liskov Substitution Principle (LSP) and the Interface Segregation Principle (ISP) are quite easy to define and exemplify, in this lesson we will talk about both of them.
Liskov Substitution Principle (LSP)
Child classes should never break the parent class' type definitions.
The concept of this principle was introduced by Barbara Liskov in a 1987 conference keynote and later published in a paper together with Jannette Wing in 1994. Their original definition is as follows:
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
Later on, with the publication of the SOLID principles by Robert C. Martin in his book Agile Software Development, Principles, Patterns, and Practices and then republished in the C# version of the book Agile Principles, Patterns, and Practices in C#, the definition became known as the Liskov Substitution Principle.
This leads us to the definition given by Robert C. Martin:
Subtypes must be substitutable for their base types.
As simple as that, a subclass should override the parent class' methods in a way that does not break functionality from a client's point of view. Here is a simple example to demonstrate the concept.
class Vehicle { function startEngine() { // Default engine start functionality } function accelerate() { // Default acceleration functionality } }
Given a class Vehicle
- it may be abstract - and two implementations:
class Car extends Vehicle { function startEngine() { $this->engageIgnition(); parent::startEngine(); } private function engageIgnition() { // Ignition procedure } } class ElectricBus extends Vehicle { function accelerate() { $this->increaseVoltage(); $this->connectIndividualEngines(); } private function increaseVoltage() { // Electric logic } private function connectIndividualEngines() { // Connection logic } }
A client class should be able to use either of them, if it can use Vehicle
.
class Driver { function go(Vehicle $v) { $v->startEngine(); $v->accelerate(); } }
Which leads us to a simple implementation of the Template Method Design Pattern as we used it in the OCP tutorial.
Based on our previous experience with the Open/Closed Principle, we can conclude that Liskov's Substitution Principle is in strong relation with OCP. In fact, "a violation of LSP is a latent violation of OCP" (Robert C. Martin), and the Template Method Design Pattern is a classic example of respecting and implementing LSP, which in turn is one of the solutions to respect OCP also.
The Classic Example of LSP Violation
To illustrate this completely, we will go with a classic example because it is highly significant and easily understandable.
class Rectangle { private $topLeft; private $width; private $height; public function setHeight($height) { $this->height = $height; } public function getHeight() { return $this->height; } public function setWidth($width) { $this->width = $width; } public function getWidth() { return $this->width; } }
We start with a basic geometrical shape, a Rectangle
. It is just a simple data object with setters and getters for width
and height
. Imagine that our application is working and it is already deployed to several clients. Now they need a new feature. They need to be able to manipulate squares.
In real life, in geometry, a square is a particular form of rectangle. So we could try to implement a Square
class that extends a Rectangle
class. It is frequently said that a child class is a parent class, and this expression also conforms to LSP, at least at first sight.
But is a Square
really a Rectangle
in programming?
class Square extends Rectangle { public function setHeight($value) { $this->width = $value; $this->height = $value; } public function setWidth($value) { $this->width = $value; $this->height = $value; } }
A square is a rectangle with equal width and height, and we could do a strange implementation like in the above example. We could overwrite both setters to set the height as well as the width. But how would that affect client code?
class Client { function areaVerifier(Rectangle $r) { $r->setWidth(5); $r->setHeight(4); if($r->area() != 20) { throw new Exception('Bad area!'); } return true; } }
It is conceivable to have a client class that verifies the rectangle's area and throws an exception if it is wrong.
function area() { return $this->width * $this->height; }
Of course we added the above method to our Rectangle
class to provide the area.
class LspTest extends PHPUnit_Framework_TestCase { function testRectangleArea() { $r = new Rectangle(); $c = new Client(); $this->assertTrue($c->areaVerifier($r)); } }
And we created a simple test by sending an empty rectangle object to area verifier and the test passes. If our Square
class is correctly defined, sending it to the Client's areaVerifier()
should not break its functionality. After all, a Square
is a Rectangle
in all mathematical sense. But is our class?
function testSquareArea() { $r = new Square(); $c = new Client(); $this->assertTrue($c->areaVerifier($r)); }
Testing it is very easy and it breaks big time. An exception is thrown to us when we run the test above.
PHPUnit 3.7.28 by Sebastian Bergmann. Exception : Bad area! #0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square)) #1 [internal function]: LspTest->testSquareArea()
So, our Square
class is not a Rectangle
after all. It breaks the laws of geometry. It fails and it violates the Liskov Substitution Principle.
I especially love this example because it not only violates LSP, it also demonstrates that object oriented programming is not about mapping real life to objects. Each object in our program must be an abstraction over a concept. If we try to map one-to-one real objects to programmed objects, we will almost always fail.
The Interface Segregation Principle
The Single Responsibility Principle is about actors and high level architecture. The Open/Closed Principle is about class design and feature extensions. The Liskov Substitution Principle is about subtyping and inheritance. The Interface Segregation Principle (ISP) is about business logic to clients communication.
In all modular applications there must be some kind of interface that the client can rely on. These may be actual Interface typed entities or other classic objects implementing design patterns like Facades. It doesn't matter which solution is used. It always has the same scope: to communicate to the client code on how to use the module. These interfaces can reside between different modules in the same application or project, or between one project as a third party library serving another project. Again, it doesn't matter. Communication is communication and clients are clients, regardless of the actual individuals writing the code.
So, how should we define these interfaces? We could think about our module and expose all the functionalities we want it to offer.
This looks like a good start, a great way to define what we want to implement in our module. Or is it? A start like this will lead to one of two possible implementations:
- A huge
Car
orBus
class implementing all the methods on theVehicle
interface. Only the sheer dimensions of such classes should tell us to avoid them at all costs. - Or, many small classes like
LightsControl
,SpeedControl
, orRadioCD
which are all implementing the whole interface but actually providing something useful only for the parts they implement.
It is obvious that neither solution is acceptable to implement our business logic.
We could take another approach. Break the interface into pieces, specialized to each implementation. This would help to use small classes that care about their own interface. The objects implementing the interfaces will be used by the different type of vehicles, like car in the image above. The car will use the implementations but will depend on the interfaces. So a schema like the one below may be even more expressive.
But this fundamentally changes our perception of the architecture. The Car
becomes the client instead of the implementation. We still want to provide to our clients ways to use our whole module, that being a type of vehicle.
Assume we solved the implementation problem and we have a stable business logic. The easiest thing to do is to provide a single interface with all the implementations and let the clients, in our case BusStation
, HighWay
, Driver
and so on, to use whatever thew want from the interface's implementation. Basically, this shifts the behavior selection responsibility to the clients. You can find this kind of solution in many older applications.
The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
However, this solution has its problems. Now all the clients depend on all the methods. Why should a BusStation
depend on the state of lights of the bus, or on the radio channels selected by the driver? It should not. But what if it does? Does it matter? Well, if we think about the Single Responsibility Principle, it is a sister concept to this one. If BusStation
depends on many individual implementations, not even used by it, it may require changes if any of the individual small implementations change. This is especially true for compiled languages, but we can still see the effect of the LightControl
change impacting BusStation
. These things should never happen.
Interfaces belong to their clients and not to the implementations. Thus, we should always design them in a way to best suite our clients. Some times we can, some times we can not exactly know our clients. But when we can, we should break our interfaces in many smaller ones, so they better satisfy the exact needs of our clients.
Of course, this will lead to some degree of duplication. But remember! Interfaces are just plain function name definitions. There is no implementation of any kind of logic in them. So the duplications is small and manageable.
Then, we have the great advantage of clients depending only and only on what they actually need and use. In some cases, clients may use and need several interfaces, that is OK, as long as they use all the methods from all the interfaces they depend on.
Another nice trick is that in our business logic, a single class can implement several interfaces if needed. So we can provide a single implementation for all the common methods between the interfaces. The segregated interfaces will also force us to think of our code more from the client's point of view, which will in turn lead to loose coupling and easy testing. So, not only have we made our code better to our clients, we also made it easier for ourselves to understand, test and implement.
Final Thoughts
LSP taught us why reality can not be represented as a one-to-one relation with programmed objects and how subtypes should respect their parents. We also put it in light of the other principles that we already knew.
ISP teaches us to respect our clients more than we thought necessary. Respecting their needs will make our code better and our lives as programmers easier.
Thank you for your time.
Comments