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: animal
database management and animal
property management. constructor
And getAnimalName
methods for managing animal
properties, and saveAnimal
methods for managing database animal
storage.
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 animal
attributes 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 animal
storing 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 Animal
like:
class Animal {
constructor(name: string){ }
getAnimalName() { }
}
复制代码
We want to traverse a animal
array, and so that each animal
emit 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);
复制代码
AnimalSound
The method does not meet the open - closed principle because it does not for a new type of animal
close objects.
If we add a new animal
object Snake
:
//...
const animals: Array<Animal> = [
new Animal('lion'),
new Animal('mouse'),
new Animal('snake')
]
//...
复制代码
We will have to modify AnimalSound
the 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 AnimalSound
process. This is a very simple example, when your program becomes large and complex, you will see that each new added animal
, the if
statement will be in the AnimalSound
repeated process is repeated until the entire application is full.
How to make that AnimalSound
method 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);
复制代码
Animal
Class now have a virtual method (Virtual Method) - makeSound
. We let each animal inherits Animal
class and implements the parent class makeSound
method.
Each animal
subclass and add in their own internal implementation of the makeSound
method. In the AnimalSound
method of traversing animal
an object array, just call each animal
object itself makeSound
method can be.
Now, if we add a animal
, AnimalSound
the method does not require any modification. We need to do is just put this new animal
object is added to the array of them.
Now AnimalSound
methods comply with the open - closed principle.
Look at an example:
Suppose you have a shop and you want to pass this Discount
class 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 Discount
class:
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 Discount
category. 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 SuperVIPDiscount
category:
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 Animal
example 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 Animal
specific type of object, and calls associated with that object leg-counting
function.
Add a new type for each Animal
class, this method must be modified so as to receive a new type of Animal
object.
//...
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 Fenton
hypothetical substitution principle Leeb several requirements:
- If the parent class (
Animal
) may be received with a parent type (Animal
method) 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 AnimalLegCount
method:
function AnimalLegCount(a: Array<Animal>) {
for(let i = 0; i <= a.length; i++) {
a[i].LegCount();
}
}
AnimalLegCount(animals);
复制代码
AnimalLegCount
Method does not concern the transfer of Animal
objects specific type, it only calls the LegCount
method. Parameter must be known only Animal
type, it may be Animal
instances of a class or an instance of its subclasses.
Now we need in the Animal
class defined LegCount
methods:
class Animal {
//...
LegCount();
}
复制代码
While its subclasses need to implement LegCount
methods:
//...
class Lion extends Animal{
//...
LegCount() {
//...
}
}
//...
复制代码
When (lion Lion
one example) is transmitted to AnimalLegCount
the time process, the method returns the number of the legs have lion.
Now you can feel, AnimalLegCount
you do not require knowledge received what type of animal
( Animal
instances of subclasses) can calculate the number of its legs, because it only needs to call the Animal
instance of a subclass of the LegCount
method. In accordance with the contract, Animal
the subclass must implement the LegCount
method.
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 IShape
interfaces:
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
}
复制代码
This interface defines the method of drawing a square, circle, rectangle. Implement IShape
the class interface Circle
, Square
, Rectangle
we 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, Rectangle
class implements the methods it simply do not have access ( drawCircle
and drawSquare
) the same, Square
the class also implements drawCircle
and drawRectangle
methods, as well as Circle
class ( drawSquare
, drawRectangle
).
If we IShape
add a method to the interface, such as drawing a triangle drawTriangle
,
interface IShape {
drawCircle();
drawSquare();
drawRectangle();
drawTriangle();
}
复制代码
All implementations of IShape
the 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 shape
object. We all interface methods can only be achieved, and throw in a method violates logic of 操作无法执行
errors.
Interface Segregation Principle does not recommend this IShape
interface design. User (in this case Rectangle
, Circle
and Square
the 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 IShape
operations performed by the interface should be handled independently by other interfaces.
In order to make our IShape
interfaces 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(){
//...
}
}
复制代码
ICircle
Only the interface is responsible for drawing a circle, IShape
is responsible for drawing any shape, ISquare
only responsible for rendering a square, IRectangle
is responsible for drawing a rectangle.
or
Subclass Circle
( Rectangle
, Square
, , Triangle等
) from the IShape
interface inheritance and implement their own draw
methods.
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 Shape
instance, 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, Http
a high-level components, which HttpService
is 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.
Http
It is forced to depend on the class XMLHttpService
categories. If we want to change the Http
connection service, you may need to be Nodejs
connected, or analog Http
service. To edit the code, we will have to wade through inspection Http
of all instances, in violation of the open - closed principle.
Http
Class should not be too concerned that you are using Http
the type of service. Let's establish a Connection
connection:
interface Connection {
request(url: string, opts:any);
}
复制代码
Connection
Interface has a request
method. With this design, we will be Connection
the instance is passed as a parameter to our Http
class:
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 Http
class Http
what 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 XMLHttpService
class that implements Connection
the 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 it to our Http
class, 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. Http
Class (Advanced Module) is dependent on Connection
the interface (abstract), in turn, Http
the type of service (lower module) is also dependent on Connection
the interface (abstract).
In addition, the Dependency Inversion principle compels us not to violate the Richter substitution principle: Connection
type Node
- XML
- MockHttpService
is 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:
- Pro TypeScript Application-Scale JavaScript Development, Steve Fenton
- S.O.L.I.D: The First 5 Principles of Object Oriented Design, Samuel Oloruntoba
Original Address
SOLID Principles every Developer Should Know , Chidume Nnamdi