[Translation] Every developer need to know SOLID principles

Object-oriented programming approach to software development has brought new design methods.

This enables developers to data having the same object / function aggregated into a class in order to achieve the sole purpose or function of the class to be implemented, regardless of the application as a whole what to do.

However, such an object-oriented programming does not completely prevent developers to write hard to understand or difficult to maintain the program. Therefore, Robert C. Martin presents five basic principles. The five principles enables developers to easily write high readability and better maintenance program.

This principle is known as SOLID five principles (the abbreviation proposed by Michael Feathers).

  • S: Single Responsibility Principle (Single Responsibility Principle)
  • O: Principles shutter (Open-Closed Principle)
  • L: Richter Substitution Principle (Liskov Substitution Principle)
  • I: Principles isolation interfaces (Interface Segregation Principle)
  • D: Dependency Inversion Principle (Dependency Inversion Principle)

Next, we discussed in detail the five principles.

Note : Most of the examples in this article may not be sufficient to meet the actual situation, or not suitable for practical applications. It all depends on your own design and use cases. But the most important thing is to understand and know how to apply and follow these principles.

Tips: similar Bit tools such as the SOLID principles into practice. It can help you organize, find, and reuse components in order to form a new application. Components can be found and shared between projects, so you can build the project faster. Git address .

Single Responsibility Principle (Single Responsibility Principle)

You only have a job. - Rocky, "Thor: Ragnarok"
a class should only have a duty

A class should only responsible thing to do. If a class has more than one responsibility, then it was more than one responsibility coupled together. A feature changed to another function will cause the occurrence of undesirable changes.

  • Note : This principle applies not only to the class, but also for component development and micro-services.

For example, the following design:

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
  saveAnimal(a: Animal) { }
}
复制代码

Animal class violates the single responsibility principle.

Why it violates the single responsibility principle?

Single responsibility principle provides that a class should be only a responsibility, but here we can see two responsibilities: animaldatabase management and animalproperty management. constructorAnd getAnimalNamemethods for managing animalproperties, and saveAnimalmethods for managing database animalstorage.

This design may cause a problem in the future?

If you need to change the program's impact on database management functions, all use the animalattributes of the class must be modified and recompiled to be compatible with the new changes,

Now you can feel the rigid system has shares of taste, like a domino effect, touching a card, it will affect all other cards.

To make this design in line with the principle of single responsibility, we want to create another class that will be responsible for animalstoring objects in the database:

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
}
//  animalDB专门负责在数据库中读写animal
class AnimalDB {
  getAnimal(a: Animal) { }
  saveAnimal(a: Animal) { }
}
复制代码

When we design class, we should be relevant feature together, so every time they tend to change, they will change because of the same reason. If the feature changes occur due to different reasons, we should try to separate them. --Steve Fenton

By properly designing these applications, our application will become in the height together.

Open - Closed Principle (Open-Closed Principle)

Software entities (classes, modules, functions) and the like should be easy to expand, but can not be modified

Let us continue to look Animallike:

class Animal {
  constructor(name: string){ }
  getAnimalName() { }
}
复制代码

We want to traverse a animalarray, and so that each animalemit a corresponding 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);
复制代码

AnimalSoundThe method does not meet the open - closed principle because it does not for a new type of animalclose objects.

If we add a new animalobject Snake:

//...
const animals: Array<Animal> = [
  new Animal('lion'),
  new Animal('mouse'),
  new Animal('snake')
]
//...
复制代码

We will have to modify AnimalSoundthe method:

//...
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);
复制代码

Now you can feel every a new animal, you need to add some new logic to the AnimalSoundprocess. This is a very simple example, when your program becomes large and complex, you will see that each new added animal, the ifstatement will be in the AnimalSoundrepeated process is repeated until the entire application is full.

How to make that AnimalSoundmethod to comply with the principle of opening and closing it?

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);
复制代码

AnimalClass now have a virtual method (Virtual Method) - makeSound. We let each animal inherits Animalclass and implements the parent class makeSoundmethod.

Each animalsubclass and add in their own internal implementation of the makeSoundmethod. In the AnimalSoundmethod of traversing animalan object array, just call each animalobject itself makeSoundmethod can be.

Now, if we add a animal, AnimalSoundthe method does not require any modification. We need to do is just put this new animalobject is added to the array of them.

Now AnimalSoundmethods comply with the open - closed principle.

Look at an example:

Suppose you have a shop and you want to pass this Discountclass to your favorite customers a discount 2 fold.

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}
复制代码

When you decide to double VIP customers a 20% discount. You can modify this Discountclass:

class Discount {
  giveDiscount() {
    if(this.customer == 'fav') {
      return this.price * 0.2;
    }
    if(this.customer == 'vip') {
      return this.price * 0.4;
    }
 }
}
复制代码

No, this design violates the Open - Closed Principle, open - closed principle prohibited to do so. If we want to give a different type of customer a new discount percentage, you will add a new logic.

In order to enable it to follow the open - closed principle, we will add a new category to expand the Discountcategory. In this new class, we will implement its new behavior:

class VIPDiscount: Discount {
  getDiscount() {
    return super.getDiscount() * 2;
  }
}
复制代码

If you're going to the Super VIP customer discount of 20%, then we can add a new SuperVIPDiscountcategory:

class SuperVIPDiscount: VIPDiscount {
  getDiscount() {
    return super.getDiscount() * 2;
  }
}
复制代码

Now you can feel, in the case without modification, we realized the extension.

Richter substitution principle (Liskov Substitution Principle)

Subclass must be able to replace its parent class.

The purpose of this principle is a subclass can determine without error replace its parent. Let all uses of the base class can be used transparently subclass, type checking if the code in the class, then it must be a violation of this principle.

We continue to use the Animalexample of the class:

//...
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);
复制代码

This code violates the substitution principle reason's (also violates the open - closed principle) - It must be up to each Animalspecific type of object, and calls associated with that object leg-countingfunction.

Add a new type for each Animalclass, this method must be modified so as to receive a new type of Animalobject.

//...
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 the method follows the Richter substitution principle, we will follow the Steve Fentonhypothetical substitution principle Leeb several requirements:

  • If the parent class ( Animal) may be received with a parent type ( Animalmethod) as a parameter. It subclass ( Pigeon) should receive a type of the parent class ( Animal) or sub-class type ( Pigeon) as a parameter.
  • If the parent class return parent class type ( Animal). Then it should return a subclass parent type ( Animal) or sub-class type ( Pigeon).

Now, we have to re-implement AnimalLegCountmethod:

function AnimalLegCount(a: Array<Animal>) {
  for(let i = 0; i <= a.length; i++) {
    a[i].LegCount();
  }
}
AnimalLegCount(animals);
复制代码

AnimalLegCountMethod does not concern the transfer of Animalobjects specific type, it only calls the LegCountmethod. Parameter must be known only Animaltype, it may be Animalinstances of a class or an instance of its subclasses.

Now we need in the Animalclass defined LegCountmethods:

class Animal {
  //...
  LegCount();
}
复制代码

While its subclasses need to implement LegCountmethods:

//...
class Lion extends Animal{
  //...
  LegCount() {
    //...
  }
}
//...
复制代码

When (lion Lionone example) is transmitted to AnimalLegCountthe time process, the method returns the number of the legs have lion.

Now you can feel, AnimalLegCountyou do not require knowledge received what type of animal( Animalinstances of subclasses) can calculate the number of its legs, because it only needs to call the Animalinstance of a subclass of the LegCountmethod. In accordance with the contract, Animalthe subclass must implement the LegCountmethod.

Interface Segregation Principle (Interface Segregation Principle)

Create a well-designed interface for a specific user.
You can not force users to rely on those interfaces that they do not use.

This principle can be used to address the shortcomings that implements the interface is too bloated.

We look at the following IShapeinterfaces:

interface IShape {
  drawCircle();
  drawSquare();
  drawRectangle();
}
复制代码

This interface defines the method of drawing a square, circle, rectangle. Implement IShapethe class interface Circle, Square, Rectanglewe must implement the methods drawCircle, drawSquare, 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 looks a little funny, Rectangleclass implements the methods it simply do not have access ( drawCircleand drawSquare) the same, Squarethe class also implements drawCircleand drawRectanglemethods, as well as Circleclass ( drawSquare, drawRectangle).

If we IShapeadd a method to the interface, such as drawing a triangle drawTriangle,

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
    drawTriangle();
}
复制代码

All implementations of IShapethe interface class needs to implement this new method, otherwise it will error.

We see that, can not use this design can instantiate a circle ( drawCircle) but can not draw a rectangle ( drawRectangle), square ( drawSquare) or triangle ( drawTriangle) of the shapeobject. We all interface methods can only be achieved, and throw in a method violates logic of 操作无法执行errors.

Interface Segregation Principle does not recommend this IShapeinterface design. User (in this case Rectangle, Circleand Squarethe like) should not be forced to rely on a method which is not needed or used. In addition, the interface segregation principle pointed out that the interface should only be responsible for a single responsibility (like the single responsibility principle), any additional behavior should be abstracted to another interface.

Here, our IShapeoperations performed by the interface should be handled independently by other interfaces.

In order to make our IShapeinterfaces in line with the principle of isolation interface, we will operate separated into 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(){
    //...
  }
}
复制代码

ICircleOnly the interface is responsible for drawing a circle, IShapeis responsible for drawing any shape, ISquareonly responsible for rendering a square, IRectangleis responsible for drawing a rectangle.

or

Subclass Circle( Rectangle, Square, , Triangle等) from the IShapeinterface inheritance and implement their own drawmethods.

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 particular Shapeinstance, such as a semicircle Semi Circle( ), a right triangle Right-Angled Triangle( ), equilateral triangle Equilateral Triangle( ), trapezoidal ( Blunt-Edged Rectangle) and the like.

Dependency Inversion Principle (Dependency Inversion Principle)

Reliance should be based on the abstract rather than concrete implementations based
A: Advanced module should not depend on low-level modules. Both should depend on abstractions.
B: Abstract should not depend on the details, should depend on the details of abstraction.

In software development we encounter a situation is mainly composed of modules of the program. When this happens, we have to use dependency injection to solve the problem. High-level components rely 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, Httpa high-level components, which HttpServiceis a lower component. This design violates the Dependency Inversion Principle:

A: Advanced module should not depend on low-level modules. It should depend on the abstract.

HttpIt is forced to depend on the class XMLHttpServicecategories. If we want to change the Httpconnection service, you may need to be Nodejsconnected, or analog Httpservice. To edit the code, we will have to wade through inspection Httpof all instances, in violation of the open - closed principle.

HttpClass should not be too concerned that you are using Httpthe type of service. Let's establish a Connectionconnection:

interface Connection {
    request(url: string, opts:any);
}
复制代码

ConnectionInterface has a requestmethod. With this design, we will be Connectionthe instance is passed as a parameter to our Httpclass:

class Http {
  constructor(private httpConnection: Connection) { }
  
  get(url: string , options: any) {
    this.httpConnection.request(url,'GET');
  }
  
  post() {
    this.httpConnection.request(url,'POST');
  }
    //...
}
复制代码

Now, no matter passed to the Httpclass Httpwhat type of service connection is that it can easily connect to the network without having to bother to understand the type of network connection.

We can now re-implement the XMLHttpServiceclass that implements Connectionthe interface:

class XMLHttpService implements Connection {
  const xhr = new XMLHttpRequest();
  //...
  request(url: string, opts:any) {
    xhr.open();
    xhr.send();
  }
}
复制代码

We can create many Http Connectiontypes and pass it to our Httpclass, without fear of error.

class NodeHttpService implements Connection {
  request(url: string, opts:any) {
    //...
  }
}

class MockHttpService implements Connection {
  request(url: string, opts:any) {
    //...
  }
}
复制代码

Now, we can see the high-level and low-level module module relies on the abstract. HttpClass (Advanced Module) is dependent on Connectionthe interface (abstract), in turn, Httpthe type of service (lower module) is also dependent on Connectionthe interface (abstract).

In addition, the Dependency Inversion principle compels us not to violate the Richter substitution principle: Connectiontype Node- XML- MockHttpServiceis transparently replace its parent Connection's.

to sum up

Here, we introduce the five principles of every software developer must comply. Make a change are usually painful, but with steady practice and perseverance, it will become part of us, and maintenance work on our programs have an enormous impact.

reference:

Original Address

SOLID Principles every Developer Should Know , Chidume Nnamdi

Guess you like

Origin juejin.im/post/5d235070e51d4510a7328132