The role of design patterns
If the function of the algorithm is to let us write efficient programs, the purpose of the design pattern is to let us write good, extensible and standard code.
SOLID design principles
In software design, we should follow some principles. SOLID five principles are very common in software design.
S principle of single responsibility
Each module is responsible for a single responsibility. If a module is responsible for multiple principles, it should be considered to divide this module
In fact, this principle seems simple, but it is difficult to distinguish whether a module is responsible for a single responsibility in software design, which may change with the change of applicable scenarios.
for instance:
class Person{ int PersonID; string name; int age; int sex; string city; string address; };
When it is used in a scenario where only user information is managed and maintained and not used for other purposes, this class meets a single responsibility.
However, when a distribution scenario is added to the business scenario, it does not meet the single responsibility. The address information of the Person should be separated.
O opening and closing principle
Open to extension and closed to modification
The opening and closing principle is used to judge whether your code is extensible. If you need to add a new function and change the existing implementation, it is a violation of the opening and closing principle. There are several levels to judge how to change the existing implementation, such as subsystem level, class level and method level. It is enough for the simpler implementation of the business to comply with the opening and closing principle at the method level. If the business is complex, it needs to consider complying with the opening and closing principle at the class level.
L Richter substitution principle.
When a subclass inherits from the parent class, we need to follow this principle: the subclass can replace any scenario where the parent class is located. If it cannot, it violates the Richter substitution principle.
For example, the parent class is bird and the child class is sparrow. Because sparrows are birds, such father and son are in line with the Richter replacement principle.
class bird{ public: bird(){ cout<<"create a bird"<<endl; } virtual ~bird(){ cout<<"destruct a bird"<<endl; } }; class sparrow : public bird{ public: sparrow(){ cout<<"create a sparrow"<<endl; } virtual ~sparrow(){ cout<<"destruct a sparrow"<<endl; } };
I interface isolation principle
The set of interfaces used by each class should be minimal.
It is easy to understand a correct example and a wrong example
class Interface{ virtual void f1() = 0; virtual void f2() = 0; virtual void f3() = 0; virtual void f4() = 0; virtual void f5() = 0; }; class A:public Interface{ public: void f1(){ cout<<"A call f1()"<<endl; } void f2(){ cout<<"A call f2()"<<endl; } void f3(){ cout<<"A call f3()"<<endl; } void f4(){ } void f5(){ } }; class B:public Interface{ public: void f1(){ cout<<"B call f1()"<<endl; } void f4(){ cout<<"B call f4()"<<endl; } void f5(){ cout<<"B call f5()"<<endl; } void f2(){ } void f3(){ } };
In this example, class a only needs methods f1, f2, f3, and class B only needs f1, f4, and f5, which all use the Interface interface. However, since five methods are defined in the Interface, class A has to implement f4 and f5 methods that it cannot use, resulting in code redundancy. At this time, we should divide the Interface into two, One is specifically used to define the methods used in a and the other is specifically used to define the methods used in B.
D
Dependency Inversion Principle
High level modules should not rely on low-level modules, and both should rely on abstraction.
This is a good strategy to ensure scalability. If it is found that the high-level module depends on the implementation of the low-level module in the system, we can consider adding a layer of abstraction to the low-level module to isolate the changes of the low-level module.
Also give an example to illustrate.
There is an order class in the ordering system, which can order different kinds of food. At this time, the order class needs to be implemented by relying on various food classes. In order not to violate the dependency inversion principle, we can add an abstract food class to make the order and various thing classes depend on this food class, which follows the dependency inversion principle, Even if new things are added later, we can just add a new specific food that depends on food, so as to encapsulate the changes.
Creative design pattern
The creation related design pattern is mainly used to solve the work of object creation. It includes singleton mode, factory mode, prototype mode and builder mode
The purpose of singleton mode is to ensure that each class can only create one instance. The implementation methods are
Rogue singleton mode, lazy singleton mode (unsafe), lock singleton mode (SAFE), double check lock singleton mode (there will be reorder problems)
Direct code
//author: Solitude //date: 2021-09-28 //purpose: descripe the design pattren about creating objects #include<iostream> #include<mutex> using namespace std; //Lazy singleton mode //C + + does not implement the static code block concept in java, so the lazy singleton mode needs to be written in a special way //This code is only used to express meaning and cannot be actually run //The disadvantage is that if a method in a class is used instead of an instance object, memory will be wasted #if 0 class Singleton1{ private: Singleton1(){} ~Singleton1(){} static Singleton1* single; static { //initialize the single } public: Singleton1* getInstance(){ return single; } }; #endif //Hungry Han single case mode //The disadvantage is that threads are not safe class Singleton2{ private: Singleton2(){} ~Singleton2(){} Singleton2* single; public: Singleton2* getInstance(){ if(single == nullptr){ //initialize the single //... } return single; } }; //Starving singleton mode (locked) //It can realize thread safety. The disadvantage is that the lock granularity is large and the efficiency is not high class Singleton3{ private: Singleton3(){ //initialize the single } ~Singleton3(){} Singleton3* single; std::mutex mtx; public: Singleton3* getInstance(){ mtx.lock(); if(single == nullptr){ Singleton3(); } mtx.unlock(); return single; } }; //The lazy double check mode is more efficient //But there is a reorder problem //reorder problem description //The constructor is generally divided into three steps: 1. Apply for memory 2. Call the constructor to initialize memory 3. Return //Some compilers initialize the constructor, postpone the execution of step 2, and return directly after applying for memory //In this way, another class may be used directly by uninitialized objects, resulting in an error //This problem cannot be solved by program, only by compiler support class Singleton4{ private: Singleton4(){ //initialize the single } ~Singleton4(){} Singleton4* single; std::mutex mtx; public: Singleton4* getInstance(){ if(single == nullptr){ mtx.lock(); if(single == nullptr){ Singleton4(); } mtx.unlock(); } return single } }; int main(){ }
Factory pattern is mainly used to solve the problem of creating multiple similar categories of objects.
The prototype pattern is mainly used to solve the problem of obtaining the same object as the current instance object
The builder mode is mainly to cope with the diversification of object construction process, and the solution is to decouple the object construction process from the object itself
This will be introduced in the next blog