SOLID: Part 3 - Liskov Substitution & Interface Segregation Principles

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.

Given a class Vehicle - it may be abstract - and two implementations:

A client class should be able to use either of them, if it can use Vehicle.

Which leads us to a simple implementation of the Template Method Design Pattern as we used it in the OCP tutorial.

template_method

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.

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.

SquareRect

But is a Square really a Rectangle in programming?

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?

It is conceivable to have a client class that verifies the rectangle's area and throws an exception if it is wrong.

Of course we added the above method to our Rectangle class to provide the area.

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?

Testing it is very easy and it breaks big time. An exception is thrown to us when we run the test above.

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.

hugeInterface

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 or Bus class implementing all the methods on the Vehicle interface. Only the sheer dimensions of such classes should tell us to avoid them at all costs.
  • Or, many small classes like LightsControl, SpeedControl, or RadioCD 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.

specializedImplementationInterface

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.

carUsingInterface

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.

oneInterfaceManyClients

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.

segregatedInterfaces

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.

Tags:

Comments

Related Articles