SOLID foundations of your code part 2

Łukasz Pawłowski
codeburst
Published in
12 min readMay 18, 2020

--

In the last article, which you can read here, we discussed two SOLID principles: the single responsibility principle and the open-closed principle. Today we’ll take a look at three other:

  • Liskov substitution principle
  • interface segregation principle
  • dependency inversion principle

All of them will help you create a better, more readable object-oriented code. They are all about designing your code, dividing code into classes/interfaces, building abstractions. If you haven’t read the previous article I encourage you to do it first and then back here.

TLDR:

  • Liskov substitution principle — if you have a specific class, you should be able to replace it with a subclass without breaking your code. Think about dependency injection. If your code requires class A and you inject class B, which inherit from class A, your code should still work as expected. You can achieve this by using abstract classes, interfaces, type hinting, etc. Make sure that more concrete implementations are compatible with parent class and do not overwrite all declarations from the parent class, but only redefine specific parts. This gives us a possibility to easily extend or exchange specific elements of functionalities without rewriting a more significant part of the system. It helps you achieved the open-closed principle.
  • Interface segregation principle — your interfaces should present specific requirements for specific usage. Your class should implement specific requirements from smaller interfaces, instead of multiple never used elements given in the broad interface. Keep in mind that, if your class implements multiple small interfaces, you probably break the single responsibility principle. By following this principle, you ensure that your classes do not contain useless functions or attributes. Also, it is easier to extend specific functionality and find where and how your class is used.
  • Dependency inversion principle — your code should depend on a more abstract element like interfaces and abstract classes instead of specific implementations. Your abstract item should not depend on specific implementations of other items. When you require some element or validate if the object you received has a particular type, use abstract classes or interfaces instead of specific implementations. This way, you can easily swap object for other.

Liskov Substitution — Derived classes must be substitutable for their base classes.

What does it mean?

Whenever you use some class, your code should be able to work with any other class which inherits from that specific class. You should create base classes that represent a more abstract approach to specific problems. Use inheritance and new classes, which extend your base class to implement more detailed and varied solutions. Implementation should be done the way that ensures compatibility between base class and subclasses, that you can replace base class with any of subclass and code should still work.

Let’s assume that you created class A and you use it in your code. You create class B, which extends class A. If you replace class A with class B and your code break, that means you broke the Liskov Substitution principle.

Liskov Substitution in practice

Let’s consider class User which represent the general user in the system:

class User {
private $password;
private $name;
private $company;
public function save(){ UserRepository::save($this); }
public final function setCompany(Client $company) { $this->company= $company; }
}class UserService {
public saveUser(User $user) {
....
$user->save();
}
public function assignCompany (User $user , string $companyName) {
ClientRepo::findByName($companyName);
$user->setCompany($company);
}
}

Now we get requirements, that we have a new type of user and it can be connected with the partner company instead of the client company. Also, we have different approaches for searching such users, so we use different Repository. All other functionalities stay as it was. If we check our class we see that it is not possible to achieve this by extending our existing class of users. We would like to extend code without changing it, but we can’t even extend correctly User class because of final declarations. If we would build user other way we would be able to achieve new functionality by simple extend User class and pass a new class to UserService:

class User {
protected $password;
protected $name;
protected $company;
public function setCompany($company) { $this->company = $company}
}class PartnerUser extends User {
....
}
class ClientUser extends User {
....
}
class UserService {
protected $userRepo;
protected $companyRepo;
public function __construct(Repository $userRepo, Repository $companyRepo){
$this->userRepo = $userRepo;
$this->companyRepo= $companyRepo;
}
public saveUser(User $user) {
....
$this->userRepo->save($user);
}
public function assignCompany (User $user , string $companyName) {
$this->companyRepo->findByName($companyName);
$user->setCompany($company);
}
}

Why?

First of all, classes that can’t be extended and replaced by their extensions make you write more code by rewriting your code from scratch or building alternatives flow. This way, part of the code will be repeated in multiple places. Repeating code makes your app hard to maintain. You need to remember where to put changes to keep the system consistent. If you make a base class with base functionality and then extend it with subclasses, it is easier to manage changes. You can easily see what is overwritten in specific situations. If your base class has base functionality ready to use, you can easily switch between it and subclasses when something needs to be changed.

If you give the possibility to overwrite your class and swap it there where it is used, you can build your functionality without breaking working elements or rewriting complex dataflows.

In the context of other principles, Liskov substitution helps you implement the open-closed principle. Let’s assume your class A gives you some functionality, which is used in class B. Class C is a subclass of class A — it’s extending it. It is easy to change how class B act with injecting class C instead of class A. But to make it possible, you should follow Liskov substitution. If you connect this principle with the open-closed principle, you should be able to create an alternative implementation of your class without changing the code where it is used.

Also, it is worth mentioning that creating base class and subclasses, which can be switched, help you build a nice hierarchical structure. The level of abstraction which gives us parent class can help us group functionalities and code. It helps us create a nice structure of code.

Additional comment

Like the previous principle, it is essential to follow that principle in libraries, frameworks, and complex parts of your systems. If your code is broken after the developer replaces your class with his own inheritance, it will make him change your library or abandon it to a more flexible solution.

You can also map it to more abstract or higher-level elements of systems. Think about microservices. Let’s assume that you have multiple microservices and want to make them as specific as possible. You have few microservices which do the same thing, for example, generate a file for a selected report but in different formats. You do not want to build a whole flow of data to get a different file format. You still want to get data to report the same way for each type of file. You still want the same calculations. You only change this file generator. So the base generator is extended by its inheritance, which can be swapped any time. As another example, think about SQL databases. You can build your app with basic SQL queries and swap MySQL to Postgre or MariaDB any time you want, and it should not break your app.

To follow this principle, you should make sure that you do not overwrite a function declaration when you extend the class. Also, make sure that your class can be extended by avoiding final functions, etc. If you create base classes to cover general problems and all concretes move to subclasses that can replace it without breaking the system, you should be able to create code fulfilling that principle.

Interface Segregation — Make fine grained interfaces that are client specific.

What does it mean?

Your interfaces should define real, usable requirements which must be fulfilled by an object in a specific situation. If a specific situation does not require some elements, the interface should not require them too. If the interface requires too many elements based on many different situations, you should divide such interface to smaller, more specific interfaces.

Interface Segregation in practice

Let’s say you implement a system to manage projects in your company. Your company does many different types of projects. Your application has a wide range of functionality from tracking time, workload, costs. You need to use the project entity almost everywhere. Each project will be implemented by a different class. So you decide to create ProjectInterface. You check where it is used and start to implement it based on different requirements and usage of Project:

interface ProjectInterface {
public function createInvoice(){}//to create invoice fo client
public function setClient($client){}
public function toJson(){}
public function toArray(){}
public function toXML(){}
public function loginToSystem(){} //to login to system build in project with currently loged in user
public function assignUsers($users){}
}

Now you start to implement your project’s classes. You start with two main types: external and internal projects. Here comes a problem. External projects never use loginToSystem because users from your system do not relate to your client’s systems. Internal projects never have clients also you never create an invoice for them. Invoice, client, and loginToSystem are used in very specific situations and it should be presented in different interfaces as different requirements. Here is how it could look:

interface ProjectInterface {
public function assignUsers($users){}
}
interface LoagableInterface {
public function loginToSystem(){}
}
interface InvoiceableInterface {
public function createInvoice(){}
}
interface OwnedByClientInterface {
public function setClient($client){}
}
interface SerializedInterface {
public function toJson(){}
public function toArray(){}
public function toXML(){}
}

Why?

Interfaces declare requirements for specific object’s type. We can ensure that objects fulfill our requirements by using interfaces. This way, we verify that elements do not break our app because of missing functions/parameters or by returning the wrong data type.

If an interface is used in many locations for different things, it makes the interface less useable. Smaller interfaces created to cover specific requirements can help you verify what requirements are and from where they came. It is easier to manage and understand such requirements. It also means that it is easier to understand why and what class should do.

You should also ask yourself — If a requirement is used in multiple places and you need to adjust them in only one place, will it broke other parts of the system? Less complex interfaces used in only a few specific places create fewer dependencies between unrelated parts of the system.

Also, your object must implement all requirements presented by an interface. If we do not use part of the object, it is a waste of time and resources. If interface requires 10 functions and you have 20 classes that implement it, and 10 of them use only 5 functions, that means something is wrong. You waste time creating functions that are never used.

To complex interface, which requires unrelated functionalities, will also make you break single responsibilities. When you require multiple functions that represent different functionalities and implement these requirements in one class, such class consists of different functionalities. If your class implements multiple small interfaces, it will be easier to find out when you should split your class into smaller ones with more direct responsibility.

Additional comment

You can move it even further — treat the interface as an access point (“interface”) to some modules, libs, functionality, or even full application. Imagine that you have microservices and APIs for them. You want to keep consistency between microservices. It does not mean that each service should follow exactly the same interface pattern (the same way to accept data, the same format of data, etc.). Each element is different, and requirements for it change depending on the place where it is used. Requirements should be defined per usage, not globally, to cover all possible usage with one set of requirements. Consistency is very important, but it should be presented at a reasonable rate. If your API needs to change a bit in specific parts, change it by breaking consistency, which makes it harder to use. You do not need to always accept the same headers in HTTP requests, or expect the same parameters in all requests. Your library can have different solutions and can follow multiple patterns instead of one if it creates redundancy.

Fulfilling that principle is relatively easy. Whenever you need to create the interface, consider why you need it and where it will be used. If the interface is used in multiple places, check what part of it is specialized. Split your interface to few — at least one with global requirements and other with specialized requirements. Try to build interfaces per specific requirement. Do not worry about a number of interfaces. You can refactor and merge them later. Check if the requirements presented by your interface are always used together. If some requirements are used separately, you can put them in a new interface.

Dependency Inversion — Depend on abstractions, not on concretions.

What does it mean?

You should depend on a more abstract concept like abstract classes or interfaces instead of a specific class or object. When you try to verify if you received proper objects, base it on the interface instead of its implementation. Also, you should not depend on specific implementations of function but a more abstract declaration of them like declaration in interfaces. Make sure to define expected input and output instead of how data manipulation should work.

Dependency Inversion in practice

Let’s say you want to create a mechanism that transforms any data passed as an object into API response. API response body needs to have a form of JSON string. But how to make sure that each object will be transformed into a correct response body? Would you build multiple API response builders and use switch/if statements? Or would you create one interface you’ll use?

class APIResponseBuilder {
public function __consturct(JsonSerializeInterface $object) {
$this->object = $object;
}
public function buildResponse() {
$response = new HTTPResponse();
...
$response->body = $this->object->toJson()
}
}
interface JsonSerializeInterface {
public functino toJson(): string {}
}

Another example can be communication with the database. If you create your data repository, you do not depend on specific driver implementation. Instead, you depend on some abstract DB manager, which based on configuration reuse specific drivers. This way you can switch your DB without changing code with application logic:

//here we depend on a specific implementation of DB driver and specific database engine
class UserRespositoriesForMysql {
public function __construct(MySQLDriver $driver) {$this->driver = $driver}
...
}
//here we depend on general db driver, we do not care about db engine, we focus on application logic
class UserRespositoriesForMysql {
public function __construct(DBDriver $driver) {$this->driver = $driver}
...
}

Why?

Depending on abstraction gives us the possibility to switch between implementations, give us flexibility. With abstract dependency, we can easily create code fulfilling Liskov substitution and open-closed principle.

Depending or specific implementation in abstract elements makes them less abstract. It forces the usage of specific solutions, which is contrary to the concept of abstraction. The reusability of such code is much lesser, because of hardcoded dependencies and restrictions created by classes.

Additional comment

This principle also can be scaled up to general dependencies of your code. When you create a logic of your application, you should try to create it independently from frameworks and libraries you use. Frameworks and libraries simplify many things, but if you can write your core code natively in a specific language, it will be easier to switch between frameworks and systems. Consider model templates engines. You can create your logic independently from the template engine. Use the template engine just for data presentation. Think about ORMs. You can create your base models and logic independently and inject ORMs repositories. It is not easy to achieve but possible.

Implementing this principle requires you to think about more abstract solutions. Start from refactoring your old code. Find any place where you create a new object from some class. Think if you can instead inject such an object from outside. Find all places where you inject some objects. In any injection type-hint, class verification, etc. use interfaces and abstract classes instead. If there is no interface or abstract class, think if you can create some interface and require it instead of your class. But do not create too many useless interfaces or abstract classes, which are just empty shells used only once.

As you see, all five principles are related, and they create one consistent ruleset. I know it is not easy to use them for the first time — after reading one or even five articles, you’ll find yourself struggling to use them correctly, or even understand how they transferred to your code. But We all were at that point — we start to follow some principles, and we all have problems understanding how to use them in real code. Trust me, using principle, and good practice can save you a lot of headaches. Try, code, talk with others, compare your code to others, and finally, you’ll make your code better.

That’s all, folks. I hope you enjoyed it.

--

--

Web developer, tech advisor, manager, husband & father. Tech Manager at Boozt