A brush to see the fun, two brush to see the door. Look at the things you don’t understand, look at it; If you understand something, you just look at it.
Overview of the Programming paradigm
Before we talk about SOLID principles, it’s important to understand the programming paradigm. If design principles are about organizing code better, programming paradigms are about giving us the choice to use different code structures.
So far, there are programming paradigms: structured programming, object-oriented programming, and functional programming. Each change in the programming paradigm has had a profound impact on later generations.
Before “programmers” came along, programming was frowned upon. How to convince others of something that can’t prove itself. Dijkstra wanted to prove the correctness of the program by mathematical derivation.
Before that, however, Bohm and Jocopini had shown that sequential structures, branching structures, and loop structures could be combined to construct any program.
This also proves that the set of control structures required to build deducible modules is equivalent to the minimum set of control structures required to build all programs.
This is where structured programming is born, and Dijkstra then proves the correctness of these structures and the code used to string them together, and then deduces the correctness of the whole program. Finally, in the structured programming paradigm, the correctness of the current program is deduced through mathematical proof and scientific falsification.
The most valuable thing about the structured programming paradigm is that it gives us the ability to create units of falsifiable program.
Speaking of object-oriented programming, what is object-oriented programming, is familiar with the three characteristics of object-oriented programming: encapsulation, inheritance and polymorphism. These features are not unique to object-oriented programming languages, and some are not even strong points of object-oriented programming languages. The only feature worth mentioning is polymorphism. In programming oriented languages, the use of polymorphism has become more secure and convenient.
Some people say that object-oriented programming is a way to model the real world. I think it is more appropriate to say that object-oriented programming is a way to model the world that people live in. In the structured programming paradigm, the correctness of large systems can be verified by breaking them down into modules and components, and then into smaller, provable functions and testing them. Now, let’s turn this process around and see how these small functions, or modules, or components, can be efficiently combined into a larger system. Humans are good at this.
As we all know, there are many Chinatowns abroad, and some chinatowns have existed for a long time. Even if the Chinese who live in Chinatowns have never been to China, it does not prevent them from communicating with native Chinese. No matter how far apart we are or how many generations we have multiplied, as long as we still recognize the Chinese civilization, we will not be unable to communicate. This sense of identity not only transcends time and space, but also gathers strength in times of need, as COVID-19 has shown.
Polymorphism in object orientation is more like the agreement of different components to a contract. Based on this recognition, we can achieve dependency inversion and get rid of the phenomenon that the control flow determines the source code dependency.
Object-oriented programming is by means of polymorphism to the dependence of the source code control ability, the ability to let the software architect can construct a plug-in architecture, allowing senior strategic component and the underlying implementation component separation, underlying components can be compiled into plug-in, achieve independence and high-level component development and deployment.
As for functional programming, the principle is simple: variables in a functional programming language are immutable.
Just reading lightweight Functional Programming in JavaScript, we can see some of the hazards of variable variability. In view of this, the application can be divided into mutable and immutable parts, with a logical focus on immutable components and an appropriate mechanism to protect mutable variables.
Event tracing is a good example of functional programming thinking.
Finally, I summarize the three paradigms:
Polymorphism is our way to cross the boundary of architecture, functional programming is our way to regulate and limit the location and access of data, and structured programming is the basis of algorithm implementation of various modules.
This aligns with the three main focuses of software architecture: functionality, component independence, and data management.
Design principles
As we have seen above, large systems can be broken down into smaller functions. What we need to do now is to combine functions and data structures into modules, aggregate the modules into components, and couple the components into systems. Whether it is a module, a component, or any other combination of data and functions, they are the smallest units on some level.
How to assemble them efficiently requires a design principle to guide them to best practices. This idea, which has no specific pattern but definite goals:
- Make software tolerant of changes.
- Make software easier to understand.
- Build components that can be reused across multiple software systems.
In the previous article, we likened the programming world to the human world. As a person in the society, people should not only play their own roles, but also play the role given by the society. Therefore, we often hear “people in rivers and lakes, involuntarily” such a feeling. In the system, as units in different meanings, it is also necessary to deal with the problems within and between units. Here, it is more about dealing with the dependency between units. In human terms, this “dependence” means something like “involvement”.
Regardless of what the design principles are, let’s talk about the importance of dependencies. (Dependencies are much the same from a component perspective.)
Component dependencies are not a concern from the get-go; they are more about building and maintaining your application. If a software system has not changed since it was built, and will not change in the future, there is no need to pay attention to design principles, even if you use these best practices.
- The Dependency Free Loop principle: There should be no loops in component dependency diagrams.
- The stable dependency principle: Dependencies must be directed in a more stable direction.
- The principle of stable abstraction: The abstraction of a component should be consistent with its stability.
The dependency Free loop principle: When components are coupled together, there should be no circular dependencies, and the components in the loop make up a larger component, which is bad for build releases.
Stable dependency principle: a maintainable system should have both mutable and stable parts, and mutable should depend on stable. This dependence is a kind of constraint as well as a kind of trust. Conversely, if unstable components are depended on by stable components, it means that unstable elements must become stable to ensure the stability of stable components, and this kind of variability will become difficult to modify. In other words, the more components depend on a component, the more it needs to be stable.
Stable abstraction principle: This requires stable components to be abstract and mutable components to be concrete. Combined with the previous principle, dependencies should be directed in a more abstract direction.
This is what components need from a component coupling perspective, but back to the main topic.
Single responsibility principle
Any software module should only be responsible for one type of actor.
For us, the most familiar is the single responsibility principle, a function only completes one function, which is the embodiment of a single responsibility in the implementation details, but it does not represent the whole of a single responsibility.
We first regard software modules as a combination of data and functions. At the beginning, when a software module is responsible for multiple types of actors, it indicates that the software module is compatible with the functions of multiple types of actors. As the diversity of multiple actors becomes more apparent, the software module adjusts more frequently. From the stable dependency principle, we already know that “the more components a component is dependent on, the more it needs to be stable.”
This principle also guides whether or not to reuse a software module. What kind of behavior is this software module responsible for? Does the software module that will depend on this module belong to this behavior? The prevailing microservices architecture does not necessarily reuse modules, even if they are common, depending on the domain.
const Employee = {
regularHours(hour) {
let fakeHour = hour * 1.1;
return fakeHour;
},
CFOHours() {
console.log('I am the CFO of the people, not to fight! Working hours:The ${this.regularHours(6)}`)
},
COOHours() {
console.log('I am the COO of the people, who dare to refuse! Working hours:The ${this.regularHours(6)}`)}}// CFO
Employee.CFOHours();
// COO
Employee.COOHours();
// I am CFO, refuse to fight! Working duration: 6.6000000000000005
// I am the COO of the people, who dare to refuse! Working duration: 6.6000000000000005
/* There are 'CFOHours' and' COOHours' in this module, and these two behaviors share 'regularHours'. If the CFO changes' regularHours' without the COO's knowledge, it will definitely affect the COO. To avoid this, separate the behaviors of different classes. * /
const Employee = {
regularHours(hour) {
let fakeHour = hour * 1.1;
returnfakeHour; }}const CFO = Object.create(Employee);
CFO.regularHours = function (hour) {
let fakeHour = hour * 1.2;
return fakeHour;
};
CFO.CFOHours = function () {
console.log('I am the CFO of the people, not to fight! Working hours:The ${this.regularHours(6)}`)}; CFO.CFOHours();const COO = Object.create(Employee);
COO.COOHours = function () {
console.log('I am the COO of the people, who dare to refuse! Working hours:The ${this.regularHours(6)}`)
}
COO.COOHours();
// I am CFO, refuse to fight! Working time: 7.199999999999999
// I am the COO of the people, who dare to refuse! Working duration: 6.6000000000000005
Copy the code
Interface Isolation Principle
Interface segregation Principles (ISP) state that a client should not rely on a method it does not use. The interface Isolation principle (ISP) aims to decouple systems so they can be easily refactured, changed, and redeployed.
Literally, single responsibility is considered from the perspective of the software module developer, and interface isolation is considered from the perspective of the software module user. If a single responsibility is a division of a certain type of behavior, the granularity of the division of behavior depends on the user of the module.
As a front-end developer, there are many examples of interface isolation violations. In project development, often using a third-party library, sometimes even if only a few methods of the library, also need to install the library all rely on, but when installing a third-party library, often will encounter the library depends on package versions incompatible problems lead to the whole project started to collapse, the incompatible depend on the method provided by the package may not used at all.
/* 'CFOHours' and' COOHours' are responsible for their own actors, but for the company, these are internal actions. Even though different types of behavior can be coupled by different users, the above hazards still exist and isolation is needed. * /
const ICFO = {
regularHours() {
console.log('ICFO Interface: regularHours')
},
CFOHours() {
console.log('ICFO interface: CFOHours')}}const ICOO = {
COOHours() {
console.log('ICOO interface: COOHours')}}const Employee = {
regularHours(hour) {
let fakeHour = hour * 1.1;
return fakeHour;
},
CFOHours(obj) {
if (Object.getPrototypeOf(obj) == ICFO) {
obj.CFOHours(this);
}
},
COOHours(obj) {
if (Object.getPrototypeOf(obj) == ICOO) {
obj.COOHours(this); }}}const CFO = Object.create(ICFO);
CFO.regularHours = function (hour) {
let fakeHour = hour * 1.2;
return fakeHour;
}
CFO.CFOHours = function (self) {
console.log('I am the CFO of the people, not to fight! Working hours:The ${this.regularHours(6)}`)
}
Employee.CFOHours(CFO)
const COO = Object.create(ICOO);
COO.COOHours = function (self) {
console.log('I am the COO of the people, who dare to refuse! Working hours:${self.regularHours(6)}`)
}
Employee.COOHours(COO)
// I am CFO, refuse to fight! Working time: 7.199999999999999
// I am the COO of the people, who dare to refuse! Working duration: 6.6000000000000005
Copy the code
The open closed principle
The open-closed principle states that “objects in software (classes, modules, functions, etc.) should be open for extension but closed for modification,” which means that an entity is allowed to change its behavior without changing its source code. Well-designed computer software should be easy to extend and resistant to modification.
This means that a good system should be stable and flexible. The so-called stability, not because of small changes in the requirements of the system will lead to large changes. It is not so much the resistance to change as the tolerance of the system to change, as can be seen from the principle of component coupling. Flexibility requires a good software system that can be easily extended without modification.
The open – close principle can be divided into two schools in concept, Meyer’s open – close principle and polymorphic open – close principle. (This time only talk about the understanding of polymorphic open and close principle)
Meyer open closed principle: Meyer’s definition advocates implementation of inheritance. Implementations can be reused by inheritance, but interface specifications need not be. The existing implementation is closed to modification, but the new implementation does not have to implement the original interface. Polymorphic open-closed principle: The definition of the polymorphic open-closed principle advocates inheritance from abstract base classes. Interface specifications can be reused through inheritance, but implementations need not. An existing interface is closed to modification, and a new implementation must, at the very least, implement that interface.
The system consists of two parts, the changing and the invariable, which are encapsulated and isolated. And for the part of the change can also seek common ground while reserving differences, the change of the same part and encapsulated invariable part of the agreement, has become a necessary factor to achieve polymorphism – contract. In fact, this contract should be defined by the higher-level components, followed by the lower-level components, which have more detailed requirements in dependency inversion.
var IHours = {
logHours() {
console.log('IHours interface: IHours')}}var Employee = {
regularHours(hour) {
let fakeHour = hour * 1.1;
return fakeHour;
},
logHours(obj) {
if (Object.getPrototypeOf(obj) == IHours) {
obj.logHours(this); }}}var CFO = Object.create(IHours);
CFO.regularHours = function (hour) {
let fakeHour = hour * 1.2;
return fakeHour;
}
CFO.logHours = function (self) {
console.log('I am the CFO of the people, not to fight! Working hours:The ${this.regularHours(6)}`)
}
Employee.logHours(CFO)
var COO = Object.create(IHours);
COO.logHours = function (self) {
console.log('I am the COO of the people, who dare to refuse! Working hours:${self.regularHours(6)}`)
}
Employee.logHours(COO)
// I am CFO, refuse to fight! Working time: 7.199999999999999
// I am the COO of the people, who dare to refuse! Working duration: 6.6000000000000005
Copy the code
Richter’s substitution principle
“Derived (subclass) objects can replace their base (superclass) objects in a program.” This principle means that if you want to build a software system with replaceable components, those components must follow the same convention so that you can replace them with each other.
If this rule focuses on substitutability, it’s pretty much the same as the last one, so let’s leave that for now. If we’re talking about inheritance, my personal understanding is that if a subclass needs to override its parent, there’s no need to inherit from that parent.
Dependency inversion principle
The Dependency Inversion principle (DIP) refers to a specific form of decoupling in which a high-level module is not dependent on the implementation details of a low-level module, and dependencies are reversed (reversed) so that low-level modules are dependent on the requirements abstraction of high-level modules.
This principle has been covered in a previous blog post and won’t be covered here. See DIP, IoC, DI, JS
conclusion
That’s all there is to it. The code implementation does not use classes but is based on behavior delegates because it is simple and easy. Design principle is a kind of thought, thought should be flexible, and should not be confined to any form of expression.
Reference:
- The Way to Clean Architecture
- JavaScript Design Patterns and Development Practices
- JavaScript You Don’t Know
- The big idea behind the small example — correct use of interfaces in the sense of “inversion” in DIP