Trong phần này, chúng ta cùng nhau tìm hiểu và triển khai 1 cách đơn giản nguyên lý trong lập trình hướng đối tượng – SOLID.
SOLID là 5 chữ cái được viết tắt của:
- Single Responsibility: 1 lớp chỉ đảm nhiệm cho một nhiệm vụ duy nhất.
- Open-Close Principle: Một class nên được thiết kế để mở rộng và đóng để sửa đổi.
- Liskov Substitution: Lớp dẫn xuất có thể được thay thế tại chỗ mà lớp cơ sở sử dụng.
- Interface Segregation: Nếu interface quá lớn thì nên tách nhỏ hơn, với nhiều mục đích cụ thể
- Dependency Inversion: Mỗi thành phần trong hệ thống (class, module, …) chỉ nên phụ thuộc vào các abstraction, không nên phụ thuộc vào các concretions hoặc implementations cụ thể
It’s totally fine to be a little puzzled with some of the above explanations which are not the exact definitions (because the exact definitions are far more perplexing). As once tweeted by Jeffrey Way of Laracasts;
Dependency Inversion, Inversion of Control, Dependency Injection, IoC Container. It’s no wonder why people get so confused. So much jargon!
Let’s try to look into each of these principles in detail, so that we can convert these jargon into slangs.
The easiest one to follow and put into practice. It’s very straight forward. e.g. I need to make an API end point in back-end which receives a request, does authentication, validates the received data, does query on database, makes a formatted response and finally, send back the response. All these tasks look trivial. So, the Single Responsibility Principle (SRP) just asks us to do these tasks in separate Classes, instead of, making functions in a single Class and doing everything at one place. If we keep all the things in one file, it will be like a kid who’s playing and messing up things and enjoying his life. Lets not be lazy to create the Classes. They take a little extra effort but it will make life easier for you and the coders who will read your code later.
For example, suppose you got a bug and you figure out that the line of code that you did might be the source of problem. Now you will have to look for a single line in a huge file. And still you are not sure if you fix this issue at one place, some other functionality using this piece of code might break! So here is a sample code to show the SRP violation in PHP:
It ensures that the code is open for extension but closed for modification. It reduces the chances of code rot i.e. editing the same piece of code again and again. It is very helpful in cases when the requirements may keep getting added in the future.
For example, an authentication module which requires users with just username and password on your website to login. You code a login module and perform login for that user.
However, you may now want the users to be able to login using Google, Facebook or other platforms, where they have already logged in and they bring with them a custom access token with which they can login. Now you have to put checks on what kind of user is that and based on that, you proceed to write the login logic.
But this clearly will make a code rot if tomorrow there is another set of users to be logged in, using another mechanism. Nobody would want to modify the code that is already written for a crucial functionality like login.
Let’s see how it will go if we follow the open-close principle while writing this code. Separate the login behaviour behind an interface and create separate Classes for normal login and third party login to flip the dependencies. Any new person who wants to write a whole different login module functionality for a different set of users will create their own login module’s Class and implement the login interface (the functions that Login interface has as agreements).
To sum up the open-close principle, if a code can be extended, then write in such a way that it doesn’t needs to be modified if extra and similar requirements come up. Easily detected when we write conditions for instance types of a Class.
It states that any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted. Basically it takes care that while coding using interfaces in our code, we not only have a contract of input that the interface receives but also the output returned by different Classes implementing that interface; they should be of same type.
e.g. Two different Classes implementing an interface return different type of values(array, collection, list). Now we will be forced to put conditional statements to check what kind of value is returned from the method we called. If its an array do this, else, if its a collection do that and so on. This kind of checks won’t be necessary if we follow Liskov substitution principle and make sure that returned exception types(if any) and the values have uniform return types.
A code snippet to show the LSP voilation :
Doc blocks can very helpful to make sure that LSP is not violated and made easy to understand what is the type of value returned from function.
Interface Segregation Principle
It states that a client must not be forced to implement an interface that it doesn’t use. It will make sure that Classes are sharing only the knowledge that is required and not just bloating themselves, just because we committed to code to interface.
At this point, I would like to name one more jargon, Fat Interface. So what is Fat Interface, basically, and interface becomes fat when it’s handling too many contracts. A fat interface violates Single Responsibility Principle too as it’s handling more than one responsibilities at a time.
Let’s take an example, suppose we have a Class Driver who has the responsibility to manage a Car.
Now the Driver Class can have a operate method which will get an instance of Car and we can call the getFuel, shiftGear and and steer methods of Car Class.
Since we are committed to code to an interface, we make the CarInterface and make the Car Class implement CarInterface to override getFuel, shiftGear and steer methods.
Now, the driver gets an electric car e.g. Tesla Model S. You make a Class ElectricCar and implement the CarInterface. Since you have a contractual binding, you have to override the methods. But an electric car doesn’t require to get fuel. Yet we will have to override this getFuel method and do nothing in that for electric cars.
This violates Interface Segregation principle. This is exactly where we need to make our fat interface, CarInterface, thin and move out the fuelling part out of CarInterface to a more appropriate interface, like CarFuelInterface and change our CarInterface to CarOperationInterface. Now, a DieselCar can implement CarOperationInterface and CarFuelInterface whereas and ElectricCar Class can implement only CarOperationInterface. An now, since Electric car requires charging, ElectricCar Class can additionally implement an CarChargeInterface with methods like plugElectricity, doCharging, unplugElectricity etc. which means that we are open to extension but closed to modification, hence adhering to open-close principle too. This example also shows that SOLID principles are interlinked to each other. Bottom line is that a Class should not have the extra knowledge that is not required.
Dependency Inversion Principle
It states that High level modules should never depend on Low level modules, instead the High level module can depend upon an abstraction and the Low level module depends on that same abstraction. It’s not the simplest statement that we have come across. In very simple words… nope, a statement this complex can’t be simplified.
So let’s take a simple example, suppose there is a PasswordReminder Class which can be a high level module(a module that doesn’t focus on detailed implementation) requires access to dbConnection property. Now how about we just inject the MySQLConnection Class to the constructor of PasswordReminder Class(dependency injection) and give it access to dbConnection property. But this will make the High level module(PasswordReminder) depend on low level module(MySQLConnection). But does the PasswordReminder need the knowledge on connection type? Nope. It doesn’t. So lets code to an interface.
Make a ConnectionInterface(an abstraction) and inject that to PasswordReminder Class’ constructor to access the dbConnection property.
Now, our MySQLConnection Class becomes DBConnection Class and it can implement the ConnectionInterface to override methods like connect. Here we just decoupled the PasswordReminder Class from MySQLConnection Class using an abstraction (ConnectionInterface) and the dependency on MySQLConnection Class is inverted using the abstraction ConnectionInterface. I’m still looking for better examples for this in real scenarios. If you can suggest any, please provide them in Comments section.
Here are the definitions of these principles as provided by Wiki:
Single Responsibility Principle: A Class should have only a single responsibility
Open/Closed Principle: Software entities … should be open for extension, but closed for modification.
Liskov Substitution Principle: Objects in a program should be replaceable with instances of their sub-types without altering the correctness of that program.
Interface Segregation Principle: Many client-specific interfaces are better than one general-purpose interface.
Dependency Inversion Principle: One should “depend upon abstractions, [not] concretions.”
All these principles are something to keep in mind while coding and doesn’t necessarily mean that we have to put each of them in practice at all times. Just keep it in mind that we have to keep an idea of what we are doing in a Class. Does it need the the knowledge that I am passing to it? While trying to answer this we might get to code to an interface and put necessary abstractions in place which will make our code adhere to the SOLID principles.