Labels: Easy SRP als construction for building subclass applications
- Solid principles that every developer should know
- Single Responsibility Principle (SRP)
- Why does it violate SRP?
- What problems will this design bring in the future?
- Open/closed principle (OCP)
- How to Make It (animalsound) conform to OCP?
- LSP)
- Interface isolation principle (ISP)
- Dependency inversion principle (DIP)
- Summary
Solid principles that every developer should know
Since then, the original author is Chidume nnamdi.
Object-Oriented Programming brings about new software development and design methods. It allows developers to combine data with the same role/function into a class for a unique purpose, regardless of the entire application.
However, this object-oriented programming cannot prevent programs that are hard to understand or cannot be maintained. As a result, Robert C. Martin has developed five guiding principles that make it easy for developers to create readable and maintainable programs. These five principles are calledS.o.l. I. d Principle(This abbreviation was proposed by Michael feathers ):
- S: single Responsibility Principle
- O: open and closed principles
- L: Lee's replacement principle
- I: interface isolation principle
- D: Dependency inversion principle
Next we will discuss in detail.
Note: most examples in this article may not meet the actual situation or can not be applied to actual applications. It depends entirely on your own design and scenarios. Most importantlyUnderstand and understand how to apply/follow these principles.
Tip: the solid principle isBuild modular, scalable, and composite packaging componentsDesigned.
Single Responsibility Principle (SRP)
A class is only responsible for one thing. If a class has multiple responsibilities, it becomes coupled. Changes to one role may lead to changes to another.
Note: This principle applies not only to classes, but also to software components and microservices.
For example, consider the following design:
class Animal { constructor(name: string){ } getAnimalName() { } saveAnimal(a: Animal) { }}
The above animal violates the single responsibility principle (SRP ).
Why does it violate SRP?
SRP pointed out that the class should have a responsibility. Here, we can come up with two responsibilities: Animal database management and animal attribute management. Constructors and getanimalname manage animal attributes, while saveanimal manages the storage of animal in the database.
What problems will this design bring in the future?
If the modification of the application affects the database management function, classes that use the animal attribute must be modified and re-compiled to adapt to this new change. This system is a little like a domino card. touching a card will affect other cards.
In order to make this class conform to SRP, we have created another class, which is responsible for storing animals in the database. This separate responsibility:
class Animal { constructor(name: string){ } getAnimalName() { }}class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { }}
When designing our classes, we should put the relevant features together so that whenever they need to be changed, they all change for the same reason. If they change for different reasons, we should try to separate them. -- Steve Hu
By using this principle properly, our applications will become highly cohesive.
Open/closed principle (OCP)
Software entities (classes, modules, and functions) should be open to extensions and closed to modifications.
Let's take the animal class as an example.
class Animal { constructor(name: string){ } getAnimalName() { }}
We want to traverse an animal list and make them sound.
//...const animals: Array<Animal> = [ new Animal(‘lion‘), new Animal(‘mouse‘)];function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == ‘lion‘) log(‘roar‘); if(a[i].name == ‘mouse‘) log(‘squeak‘); }}AnimalSound(animals);
The function animalsound does not conform to the open/closed principle because it cannot be disabled for new animals. If we add a new animal snake:
//...const animals: Array<Animal> = [ new Animal(‘lion‘), new Animal(‘mouse‘), new Animal(‘snake‘)]//...
We have to modify the animalsound function:
//...function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == ‘lion‘) log(‘roar‘); if(a[i].name == ‘mouse‘) log(‘squeak‘); if(a[i].name == ‘snake‘) log(‘hiss‘); }}AnimalSound(animals);
As you can see, for each new animal, a new logic is added to the animalsound function. This is a very simple example. When applications become large and complex, you will see that each time you add a new animal, the IF statement must be repeated in the animalsound function.
How to Make It (animalsound) conform to OCP?
class Animal { makeSound(); //...}class Lion extends Animal { makeSound() { return ‘roar‘; }}class Squirrel extends Animal { makeSound() { return ‘squeak‘; }}class Snake extends Animal { makeSound() { return ‘hiss‘; }}//...function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { log(a[i].makeSound()); }}AnimalSound(animals);
Animal now has a virtual method makesound. Let's extend the animal class for every animal and implement the makesound method.
Each animal has its own voice method (makesound. Animalsound traverses the animal array and calls the makesound method of each animal.
Now, if we add a new animal, animalsound does not need to be modified. What we need to do is add new animals to the animal array.
The animalsound method conforms to the OCP principle.
Here is another example. If you have a store, you can use the following class to give your favorite customers a 20% discount:
class Discount { giveDiscount() { return this.price * 0.2 }}
When you decide to double the discount (40%) for VIP customers, you may modify the class as follows:
class Discount { giveDiscount() { if(this.customer == ‘fav‘) { return this.price * 0.2; } if(this.customer == ‘vip‘) { return this.price * 0.4; } }}
This violates the OCP principle. OCP prohibits this operation. If you want to give different types of customers a new discount percentage, you have to add a new logic.
To keep it compliant with the OCP principle, we will create a class to expand discount. In this new class, we will re-implement its behavior:
class VIPDiscount: Discount { getDiscount() { return super.getDiscount() * 2; }}
If you decide to give a 80% discount to super VIP customers, the Code is as follows:
class SuperVIPDiscount: VIPDiscount { getDiscount() { return super.getDiscount() * 2; }}
In this case, the extension is not modified.
LSP)
Subclass must be able to replace its superclass.
The purpose of this principle is to ensure that the subclass can replace its superclass without errors. If you find that your code is checking the type of the class, it must violate this principle.
Let's take animal as an example.
//...function AnimalLegCount(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == Lion) log(LionLegCount(a[i])); if(typeof a[i] == Mouse) log(MouseLegCount(a[i])); if(typeof a[i] == Snake) log(SnakeLegCount(a[i])); }}AnimalLegCount(animals);
The above method violates the LSP principle (and also the OCP principle ). It must know each animal type and call the corresponding number leg function.
You must modify this function every time you create a new animal class:
//...class Pigeon extends Animal {}const animals[]: Array<Animal> = [ //..., new Pigeon();]function AnimalLegCount(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == Lion) log(LionLegCount(a[i])); if(typeof a[i] == Mouse) log(MouseLegCount(a[i])); if(typeof a[i] == Snake) log(SnakeLegCount(a[i])); if(typeof a[i] == Pigeon) log(PigeonLegCount(a[i])); }}AnimalLegCount(animals);
In order to make this function conform to the LSP principle, we will follow the LSP requirements proposed by Steve fabric:
If a super class (animal) has a method that accepts the parameter of the super class type (anima), its subclass (PIGEON) should accept the super class type (animal type) or the Child class type (PIGEON type) as a parameter.
If a superclass returns an animal type, its subclass should return an animal type or pigeon type ).
Now, we can re-implement the animallegcount function:
function AnimalLegCount(a: Array<Animal>) { for(let i = 0; i <= a.length; i++) { a[i].LegCount(); }}AnimalLegCount(animals);
The animallegcount function does not care about the passed animal type. It only calls the legcount method. It only knows that the parameter must be of the animal type, either the animal class or its subclass.
Now, the animal class must implement/define a legcount method:
class Animal { //... LegCount();}
Its subclass must implement the legcount method:
//...class Lion extends Animal{ //... LegCount() { //... }}//...
When it is passed to the animallegcount function, it returns the number of legs of a lion.
As you can see, animallegcount can return the number of legs without knowing the animal type. It only calls the legcount method of the animal type, because according to the Conventions, A subclass of the animal class must implement the legcount function.
Interface isolation principle (ISP)
Create a fine-grained client-specific interface. Clients should not be forced to rely on interfaces they do not use.
This principle aims to overcome the disadvantages of implementing large interfaces. Let's take a look at the following ishape interface:
interface IShape { drawCircle(); drawSquare(); drawRectangle();}
This interface can draw squares, circles, and rectangles. Classes like circle, square, and rectangle that implement the ishape interface must define methods such as drawcircle (), drawsquare (), and drawrectangle ().
class Circle implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }class Square implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }class Rectangle implements IShape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }
The above code is very interesting. Class rectangle implements the methods drawcircle and drawsquare that are not used. Similarly, square implements drawcircle and drawrectangle, and circle implements drawsquare and drawrectangle.
If we add another method to the ishape interface, such as drawtriangle ():
interface IShape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle();}
Then, these classes must implement new methods, otherwise an error will be thrown.
We can see that it is impossible to implement such a shape class. It can draw circles, but not rectangles, squares, or triangles. We can throw only one error during implementation, indicating that the operation cannot be executed.
ISP opposed this design of the ishape interface. Clients (rectangle, circle, and square) should not be forced to rely on methods they do not need or do not use. In addition, the ISP points out that the interface should only execute one task (just like the SRP principle), and any additional behavior should be abstracted to another interface.
Here, our ishape interface executes the operations that should be handled independently by other interfaces. To enable the ishape interface to comply with the ISP principles, we will isolate operations on different interfaces:
interface IShape { draw();}interface ICircle { drawCircle();}interface ISquare { drawSquare();}interface IRectangle { drawRectangle();}interface ITriangle { drawTriangle();}class Circle implements ICircle { drawCircle() { //... }}class Square implements ISquare { drawSquare() { //... }}class Rectangle implements IRectangle { drawRectangle() { //... } }class Triangle implements ITriangle { drawTriangle() { //... }}class CustomShape implements IShape { draw(){ //... }}
The icircle interface only processes circle painting. ishape processes drawing of any shape. isquare only processes square painting, and irectangle processes rectangle painting.
Alternatively, the class (circle, rectangle, square, and triangle) must inherit the ishape interface and implement its own rendering behavior.
class Circle implements IShape { draw(){ //... }}class Triangle implements IShape { draw(){ //... }}class Square implements IShape { draw(){ //... }}class Rectangle implements IShape { draw(){ //... }}
Then, we can use the I-interface to create a specific shape, such as semi-circle, right triangle, equi triangle, and blunt side rectangle.
Dependency inversion principle (DIP)
Dependencies should be abstract, not specific.
Advanced modules should not depend on low-level modules. Both of them should depend on abstraction.
Abstraction should not depend on details. Details should depend on abstraction.
In software development, our applications are ultimately composed of modules. In this case, we must use dependency injection. Advanced components depend on low-level components.
class XMLHttpService extends XMLHttpRequestService {}class Http { constructor(private xmlhttpService: XMLHttpService) { } get(url: string , options: any) { this.xmlhttpService.request(url,‘GET‘); } post() { this.xmlhttpService.request(url,‘POST‘); } //...}
Here, HTTP is an advanced component, while httpservice is a low-level component. This design violates dip a: advanced modules should not depend on low-level modules. It should depend on its abstraction.
The HTTP class is forced to depend on the xmlhttpservice class. If we want to modify the HTTP Connection Service, we may want to connect to the Internet through nodejs, or even simulate the HTTP service. We will have to traverse all HTTP instances to edit code, which violates the OCP principle.
HTTP class should not care about the type of the HTTP service used. We made a connection interface:
interface Connection { request(url: string, opts:any);}
The connection interface has a request method. With this interface, we can pass a connection type parameter to the HTTP class:
class Http { constructor(private httpConnection: Connection) { } get(url: string , options: any) { this.httpConnection.request(url,‘GET‘); } post() { this.httpConnection.request(url,‘POST‘); } //...}
Therefore, no matter what type of HTTP Connection Service is passed to the HTTP class, it can easily connect to the network without knowing the type of network connection.
Now we re-implement the xmlhttpservice class to implement the connection interface:
class XMLHttpService implements Connection { const xhr = new XMLHttpRequest(); //... request(url: string, opts:any) { xhr.open(); xhr.send(); }}
We can create many HTTP connection types and pass them to the HTTP class without worrying about errors.
class NodeHttpService implements Connection { request(url: string, opts:any) { //... }}class MockHttpService implements Connection { request(url: string, opts:any) { //... } }
Now, we can see that both the advanced and low-level modules depend on abstraction. The HTTP class (Advanced module) depends on the connection interface (abstraction), while the HTTP service type (low-level module) also depends on the connection interface (abstraction ).
In addition, the dip principle will force us to follow the line replacement principle: Connection Type node-XML-mockhttpservice can replace their parent type connections.
Summary
This article describes the five principles that each software developer must follow. First, adherence to all these principles may be daunting, but as we continue to practice and stick to them, they will become part of us and will have a huge impact on application maintenance.
If you think there is any need to add, correct, or delete these principles, please leave a message in the comment area below. I would like to discuss them with you!
Original English:
Https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688
Solid principles that every developer should know