Design mode (II) 23 design modes
Rookie tutorial design mode
Design mode interview questions Express Edition
All references in this article are from the design pattern GoF
Component collaboration mode
Strategy mode
- Have the concept of "timeline"
motivation
- Support often changes
- Change the algorithm transparently to decouple the algorithm from the object itself
definition
Define a series of algorithms, encapsulate them, and make them replaceable (changeable) with each other. This pattern allows the algorithm to change (expand, subclass) independently of the client program using it
understand:
//Realize demand - > tax law strategy, add a country's strategy //Follow the open and closed principle to meet the reusability class TaxStrategy{ public: virtual double Calculate(const Context& context)=0; virtual ~TaxStrategy(){} }; class CNTax : public TaxStrategy{ public: virtual double Calculate(const Context& context){ //*********** } }; class USTax : public TaxStrategy{ public: virtual double Calculate(const Context& context){ //*********** } }; class DETax : public TaxStrategy{ public: virtual double Calculate(const Context& context){ //*********** } }; //extend //********************************* class FRTax : public TaxStrategy{ public: virtual double Calculate(const Context& context){ //......... } }; class SalesOrder{ private: TaxStrategy* strategy; public: SalesOrder(StrategyFactory* strategyFactory){ this->strategy = strategyFactory->NewStrategy();//Runtime } ~SalesOrder(){ delete this->strategy; } public double CalculateTax(){ //... Context context(); double val = strategy->Calculate(context); //Polymorphic call //... } };
- Improve reusability
- It provides an alternative to conditional judgment statements, eliminating conditional judgment statements and decoupling
- If the Strategy object has no instance variables, each context can share the same Strategy object, thus saving object overhead
Observer mode
motivation
- In the process of software construction, it is necessary to establish "Notification dependency" - when the state of an object changes, all dependent objects (observer objects) will be notified. If such dependencies are too close, the software will not be able to resist change well
- Using object-oriented technology, this dependency can be weakened and a stable dependency can be formed. So as to realize the loose coupling of software architecture.
definition
Define a one to many (changing) dependency between objects so that when the state of an object changes, all objects that depend on it are notified and automatically updated
understand:
//Design scenario -- > design large file transfer progress bar //MainForm.cpp class MainForm : public Form, public IProgress//Support multiple inheritance { TextBox* txtFilePath; TextBox* txtFileNumber; ProgressBar* progressBar; public: void Button1_Click(){ string filePath = txtFilePath->getText(); int number = atoi(txtFileNumber->getText().c_str()); ConsoleNotifier cn; FileSplitter splitter(filePath, number); splitter.addIProgress(this); //Subscription notification splitter.addIProgress(&cn); //Subscription notification splitter.split(); splitter.removeIProgress(this); } virtual void DoProgress(float value){ progressBar->setValue(value); } }; class ConsoleNotifier : public IProgress { public: virtual void DoProgress(float value){ cout << "."; } };
//FileSplitter.cpp class IProgress{//Detailed implementation public: virtual void DoProgress(float value)=0; virtual ~IProgress(){} }; //The above is implemented in the mainform file above //Abstract interface for progress notification class FileSplitter { string m_filePath; int m_fileNumber; List<IProgress*> m_iprogressList; // Abstract notification mechanism, supporting multiple observers (multiple, multiple!!!) public: FileSplitter(const string& filePath, int fileNumber) : m_filePath(filePath), m_fileNumber(fileNumber){ } void split(){ //1. Read large files //2. Write to small files in batches for (int i = 0; i < m_fileNumber; i++){ //... float progressValue = m_fileNumber; progressValue = (i + 1) / progressValue; onProgress(progressValue);//Send notification } } void addIProgress(IProgress* iprogress){ m_iprogressList.push_back(iprogress); } void removeIProgress(IProgress* iprogress){ m_iprogressList.remove(iprogress); } protected: virtual void onProgress(float value){ List<IProgress*>::iterator itor=m_iprogressList.begin(); while (itor != m_iprogressList.end() ) (*itor)->DoProgress(value); //Update progress bar itor++; } } };
Single responsibility model
Decorator mode
motivation
- Solve the problem of "excessive use of inheritance to extend the function of objects", and due to the static characteristics introduced by inheritance, the extension method lacks flexibility, and with the increase of subclasses, the combination of various subclasses will lead to more subclass expansion.
- Dynamically implement "object function extension" as needed
definition
Dynamically (compositely) add some additional responsibilities to an object. In terms of adding functions, decoration mode is more flexible than generating subclasses (inheritance) (eliminate rereading code & reduce the number of subclasses)
understand:
Deal with the scenario of "encryption operation is required at every step"
Save scale
//Business operation class Stream{ public: virtual char Read(int number)=0; virtual void Seek(int position)=0; virtual void Write(char data)=0; virtual ~Stream(){} }; //Subject class class FileStream: public Stream{ public: virtual char Read(int number){ //Read file stream } virtual void Seek(int position){ //Locate file stream } virtual void Write(char data){ //Write file stream } }; class NetworkStream :public Stream{ public: virtual char Read(int number){ //Read network flow } virtual void Seek(int position){ //Location network flow } virtual void Write(char data){ //Write network stream } }; class MemoryStream :public Stream{ public: virtual char Read(int number){ //Read memory stream } virtual void Seek(int position){ //Locate memory stream } virtual void Write(char data){ //Write memory stream } }; //Extended operation DecoratorStream: public Stream{ protected: Stream* stream;//... DecoratorStream(Stream * stm):stream(stm){ } }; class CryptoStream: public DecoratorStream { public: CryptoStream(Stream* stm):DecoratorStream(stm){ } virtual char Read(int number){ //Additional encryption operations stream->Read(number);//Read file stream } virtual void Seek(int position){ //Additional encryption operations stream::Seek(position);//Locate file stream //Additional encryption operations } virtual void Write(byte data){ //Additional encryption operations stream::Write(data);//Write file stream //Additional encryption operations } }; class BufferedStream : public DecoratorStream{ Stream* stream;//... public: BufferedStream(Stream* stm):DecoratorStream(stm){ } //... }; void Process(){ //Running fashion FileStream* s1=new FileStream(); CryptoStream* s2=new CryptoStream(s1); BufferedStream* s3=new BufferedStream(s1); BufferedStream* s4=new BufferedStream(s2); }
Bridge mode
motivation
- Because some types inherently implement logic, they have changes in two or more dimensions
- Solve the problem of dealing with "multi-dimensional change"
definition
Separate the abstract part (business function) from the implementation part (platform implementation), so that they can change independently
understand:
”Merge congeners“
//Simple communication implementation //Compared with the original function to realize the bloated state of a class (full of details and no abstraction), it is now much refined class Messager{ protected: MessagerImp* messagerImp;//... public: virtual void Login(string username, string password)=0; virtual void SendMessage(string message)=0; virtual void SendPicture(Image image)=0; virtual ~Messager(){} }; class MessagerImp{ public: virtual void PlaySound()=0; virtual void DrawShape()=0; virtual void WriteText()=0; virtual void Connect()=0; virtual MessagerImp(){} }; //Platform implementation n class PCMessagerImp : public MessagerImp{ public: virtual void PlaySound(){ //********** } virtual void DrawShape(){ //********** } virtual void WriteText(){ //********** } virtual void Connect(){ //********** } }; class MobileMessagerImp : public MessagerImp{ public: virtual void PlaySound(){ //========== } virtual void DrawShape(){ //========== } virtual void WriteText(){ //========== } virtual void Connect(){ //========== } }; //Business abstraction m //Number of classes: 1+n+m class MessagerLite :public Messager { public: virtual void Login(string username, string password){ messagerImp->Connect(); //........ } virtual void SendMessage(string message){ messagerImp->WriteText(); //........ } virtual void SendPicture(Image image){ messagerImp->DrawShape(); //........ } }; class MessagerPerfect :public Messager { public: virtual void Login(string username, string password){ messagerImp->PlaySound(); //******** messagerImp->Connect(); //........ } virtual void SendMessage(string message){ messagerImp->PlaySound(); //******** messagerImp->WriteText(); //........ } virtual void SendPicture(Image image){ messagerImp->PlaySound(); //******** messagerImp->DrawShape(); //........ } }; void Process(){ //Running fashion MessagerImp* mImp=new PCMessagerImp(); Messager *m =new Messager(mImp); }
Object creation mode
The "object creation" mode bypasses new to avoid tight coupling (depending on specific classes) caused by object creation (New), so as to support the stability of object creation. It is the first step after interface abstraction.
Factory Method
motivation
- In software systems, we often face the task of creating objects; Due to changes in requirements, the specific types of objects to be created often change.
- How to deal with this change? How to bypass the conventional object creation method (new) and provide a "encapsulation mechanism" to avoid the tight coupling between the client program and this "concrete object creation work"
definition
Define an interface for creating objects and let subclasses decide which class to instantiate. Factory Method delays the instantiation of a class (purpose: decoupling, means: virtual function) to subclasses.
understand:
//problem //Such as file segmentation //Create an abstract class class ISplitter{ public: virtual void split()=0; virtual ~ISplitter(){}; }; //The following implements a concrete class based on the abstract class class BinarySplitter : public ISplitter{ }; //In the general class, the implementation of this call through new is essentially dependent on the specific class binaryS ISplitter * splitter= new BinarySplitter;
//terms of settlement //new is a compiled dependency, and virtual implements runtime dependency //Add factory base class class SplitterFactory{ public: virtual ISplitter* CreateSplitter()=0; virtual ~SplitterFactory(){}; } //Call in general class class MainForm : public Form{ SplitterFactory* factory;//factory public: MainForm(SplitterFactory* factory){ this->factory=factory; }//The constructor of mainform class, and the specific function type is passed in by the outside world during construction void Button_Click(){ ISplitter * splitter= factory->CreateSplitter();//Polymorphic new splitter - > split(); } } //Add specific class factory class BinarySlpitterFactory: pubilc SlitterFactory{ public: virtual ISplitter* CreateSplitter(){ return new BinarySplitter(); } }; //In essence, it is to drive out "change" and lock it in a local place //Now you can just add subclasses and subclass factories //The disadvantage is that the creation method / parameters are required to be the same
Abstract Factory
motivation
- In software systems, we often face the creation of "a series of interdependent objects"; At the same time, due to the change of requirements, there are often more series of objects to be created.
- How to deal with this change? How to bypass the conventional object creation method (new) and provide a "encapsulation mechanism" to avoid the tight coupling between the client program and this "multi series concrete object creation work"?
definition
Provide an interface that is responsible for creating a series of "related or interdependent objects" without specifying their specific classes.
understand:
//problem class EmployeeDAO{ public: vector<EmployeeDO> GetEmployees(){ SqlConnection* connection = new SqlConnection(); connection->ConnectionString = "..."; SqlCommand* command = new SqlCommand(); command->CommandText="..."; command->SetConnection(connection); SqlDataReader* reader = command->ExecuteReader(); while (reader->Read()){ } } }; //Inconvenient to change and unstable
//Base classes related to database access class IDBConnection{ }; class IDBCommand{ }; class IDataReader{ }; //Combine the original three factory base classes class IDBFactory{ public: virtual IDBConnection* CreateDBConnection()=0; virtual IDBCommand* CreateDBCommand()=0; virtual IDataReader* CreateDataReader()=0; }; //Support SQL Server class SqlConnection: public IDBConnection{ }; class SqlCommand: public IDBCommand{ }; class SqlDataReader: public IDataReader{ }; class SqlDBFactory:public IDBFactory{ public: virtual IDBConnection* CreateDBConnection()=0; virtual IDBCommand* CreateDBCommand()=0; virtual IDataReader* CreateDataReader()=0; }; //Support Oracle class OracleConnection: public IDBConnection{ }; class OracleCommand: public IDBCommand{ }; class OracleDataReader: public IDataReader{ }; class EmployeeDAO{ IDBFactory* dbFactory; public: vector<EmployeeDO> GetEmployees(){ IDBConnection* connection = dbFactory->CreateDBConnection(); connection->ConnectionString("..."); IDBCommand* command = dbFactory->CreateDBCommand(); command->CommandText("..."); command->SetConnection(connection); //Relevance IDBDataReader* reader = command->ExecuteReader(); //Relevance while (reader->Read()){ } } };
-
If there is no need to deal with the demand change of "multi series object construction", it is not necessary to use the Abstract Factory mode. At this time, it is entirely possible to use a simple factory
-
"Series of objects" refers to the interdependence or interaction between objects under a specific series. Objects of different series cannot depend on each other.
-
Abstract Factory mode is mainly to deal with the demand changes of "new series". Its disadvantage is that it is difficult to cope with the demand changes of "new objects".
Prototype prototype pattern
motivation
- In software systems, we often face the creation of "some complex objects". Due to the change of requirements, these objects often face the change of clustering, but they have relatively stable and consistent interfaces
- How to deal with this change? How to "isolate" these volatile objects from the client program, so that the "client program relying on these volatile objects" does not change with the change of requirements
definition
Use prototype instances to specify the kind of objects to create, and then create new objects by copying these prototypes
understand:
//For the corresponding scenario of factory mode //Merge abstract class and abstract base class“ //abstract class class ISplitter{ public: virtual void split()=0; virtual ISplitter* clone()=0; virtual ~ISplitter(){} }; //concrete class class BinarySplitter: public ISplitter{ public: virtual ISplitter* clone(){ return new BinarySplitter(*this); } }; //MainForm class MainForm : public Form{ ISplitter* prototype;//Prototype object public: MainForm(ISplitter* prototype){ this->prototype=prototype; } //The prototype object cannot be used directly. The prototype object is used for clone void Button_Click(){ ISplitter * splitter= prototype->clone();//Clone prototype splitter->split(); } }
The focus is on clone, which sometimes uses serialization in the framework to realize deep copy
builder mode
Relatively small
motivation
- In software systems, sometimes we are faced with the creation of "a complex object", which is usually composed of sub objects of each part with a certain algorithm; Due to the change of requirements, each part of this complex object often faces drastic changes, but the algorithm that combines them is relatively stable
- How to deal with this change? How to provide a "encapsulation mechanism" to isolate the changes of "various parts of complex objects", so as to keep the "stable construction algorithm" in the system unchanged with the change of requirements?
definition
Separate the construction of a complex object from its representation, so that the same construction process (stable) can create different representations (changes)
understand:
//problem //Build a house in the game scene //Separate house from housebuilder class House{//abstract class //.... }; class HouseBuilder {//abstract class public: House* GetResult(){ return pHouse; } virtual ~HouseBuilder(){} protected: House* pHouse; virtual void BuildPart1()=0; virtual void BuildPart2()=0; virtual void BuildPart3()=0; virtual void BuildPart4()=0; virtual void BuildPart5()=0; }; class StoneHouse: public House{ }; class StoneHouseBuilder: public HouseBuilder{ protected: virtual void BuildPart1(){ //pHouse->Part1 = ...; } virtual void BuildPart2(){ } virtual void BuildPart3(){ } virtual void BuildPart4(){ } virtual void BuildPart5(){ } }; //A class should not be too fat“ class HouseDirector{ public: HouseBuilder* pHouseBuilder; HouseDirector(HouseBuilder* pHouseBuilder){ this->pHouseBuilder=pHouseBuilder; } House* Construct(){ pHouseBuilder->BuildPart1(); for (int i = 0; i < 4; i++){ pHouseBuilder->BuildPart2(); } bool flag=pHouseBuilder->BuildPart3(); if(flag){ pHouseBuilder->BuildPart4(); } pHouseBuilder->BuildPart5(); return pHouseBuilder->GetResult(); } };
- In the Builder mode, pay attention to the difference of calling virtual functions in constructors in different languages (c + + vs. C #). Virtual functions cannot be called directly when C + + classes are constructed, but C # can
Object performance mode
Singleton singleton mode
motivation
- In software systems, special classes are often used. Only by ensuring that they have only one instance in the system can we ensure their logical correctness and good efficiency
- This should be the responsibility of the class designer, not the user
definition
Ensure that there is only one instance of a class and provide a global access point for that instance
understand
class Singleton{ private: Singleton(); Singleton(const Singleton& other); public: static Singleton* getInstance(); static Singleton* m_instance; }; Singleton* Singleton::m_instance=nullptr; //Thread unsafe version Singleton* Singleton::getInstance() { if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; } //Thread safe version, but the cost of locking is too high Singleton* Singleton::getInstance() { Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; } //Double check lock, but because the memory read-write reorder is not safe, it will lead to the invalidation of the double check lock Singleton* Singleton::getInstance() { if(m_instance==nullptr){ Lock lock; if (m_instance == nullptr) {//Judge whether it is empty m_instance = new Singleton(); } } return m_instance; } //To solve the reorder problem, the compiler needs to be optimized //Cross platform implementation after C + + version 11 (volatile) std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire);//Get memory fence if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release);//Free memory fence m_instance.store(tmp, std::memory_order_relaxed); } } return tmp; }
Flyweight meta mode
motivation
-
The problem of using pure object scheme in software system is that a large number of fine-grained objects will soon fill the system, resulting in high running time price - mainly the cost of memory demand
-
While avoiding a large number of fine-grained object problems, external clients can still operate transparently in an object-oriented manner
definition
Use sharing technology to effectively support a large number of fine-grained objects
understand:
class Font { private: //unique object key string key; //object state //.... public: Font(const string& key){ //... } }; class FontFactory{ private: map<string,Font* > fontPool; public: Font* GetFont(const string& key){ map<string,Font*>::iterator item=fontPool.find(key); if(item!=footPool.end()){ return fontPool[key]; } else{ Font* font = new Font(key); fontPool[key]= font; return font; } } void clear(){ //... } };
Interface isolation mode
Add a layer of indirect interface to solve the problem
Facade (French) facade mode
motivation
Give him a "Facade" to isolate the content, and all requirements of external customers can contact the inside through the facade
definition
Provide a consistent (stable) interface for a group of interfaces in the subsystem. The facade mode defines a high-level interface, which makes a subsystem easier to use (reuse).
understand
-
Simplify the system interface and achieve a "decoupling" effect for the internal and external client programs of components - any change in the internal subsystem will not affect the change of the facade interface
-
Facade design pattern pays more attention to the whole system from the architecture level, rather than the level of a single class. Facade is more often an architecture design mode
-
The facade design pattern is not a container, and any number of objects can be placed at will. The interior of the components in the facade mode should be "some columns of components with large mutual coupling relationship", rather than a collection of functions
Proxy mode
motivation
- In object-oriented systems, direct access to some objects will bring a lot of trouble to users or system structure for some reasons (such as high cost of object creation, or some operations need security control, or need out of process access, etc.).
- It is a common solution in software development to manage / control the unique complexity of these objects without losing transparent operation objects and adding an indirect layer
definition
Provide a proxy for other objects to control (isolate, use interfaces) access to this object.
understand
//problem class ISubject{ public: virtual void process(); }; class RealSubject: public ISubject{ public: virtual void process(){ //... } }; class ClientApp{ ISubject* subject; public: ClientApp(){ subject=new RealSubject();//Cannot generate directly } void DoTack(){ //... subject->process(); //... } };
//resolvent class ISubject{ public: virtual void process(); }; //A representation of proxy class SubjectProxy: public ISubject{ public: virtual void process(){ //... An introduction to RealSubject } }; class ClientApp{ ISubject* subject; public: ClientApp(){ subject=new SubjectProxy(); } void DoTack(){ //... subject->process(); //... } };
- The implementation granularity of specific design patterns varies greatly, and some may do fine-grained control over a single object
- It is essentially an indirect method
Adapter
There are many applications in C + + standard library
motivation
- Due to the change of application environment, sometimes "some existing objects" need to be applied in the new environment, but the interface required by the new environment is not satisfied by the existing objects
- Responding to changes in migration“
definition
Convert the interface of a class into another interface that the customer wants. The Adapter mode enables those classes that are incompatible with the interface and cannot work together to work together
understand
//Target interface (new interface) class ITarget{ public: virtual void process()=0; }; //Legacy interface (old interface) class IAdaptee{ public: virtual void foo(int data)=0; virtual int bar()=0; }; //Legacy type class OldClass: public IAdaptee{ //.... }; //object adapter class Adapter: public ITarget{ //inherit protected: IAdaptee* pAdaptee;//combination public: Adapter(IAdaptee* pAdaptee){ this->pAdaptee=pAdaptee; } virtual void process(){ int data=pAdaptee->bar(); pAdaptee->foo(data); } }; //Class Adapter class Adapter: public ITarget, protected OldClass{ //Multiple inheritance } int main(){ IAdaptee* pAdaptee=new OldClass(); ITarget* pTarget=new Adapter(pAdaptee); pTarget->process(); } class stack{ deqeue container; }; class queue{ deqeue container; };
- ”If you want to reuse some existing classes, but the interface is inconsistent with the requirements of the reuse environment "
- Object adapter and class adapter. However, the class adapter adopts the "multi inheritance" implementation method, which is generally not recommended and inflexible
- The adapter pattern can be implemented very flexibly, and there is no need to stick to the two interfaces defined in Cof23.
Mediator mediator model
motivation
To solve the problem that multiple objects interact with each other, a complex reference relationship is often maintained between objects. If some requirements change, this direct application relationship will face constant changes. Use a mediation object to do "indirect"
definition
A mediation object is used to encapsulate (encapsulate changes) a series of object interactions. The mediator makes the mutual references of objects that do not need to be displayed (compile time dependency - > run-time dependency), so that they are loosely coupled (manage changes), and the interaction between them can be changed independently.
understand
Decouple the complex association relationship between multiple objects
State change mode
In component construction, the state of some objects often faces changes. How to effectively manage these changes? While maintaining the stability of high-level modules? The state change pattern provides a solution to this problem
State mode
- In the process of software construction, if the state of some objects changes, their behavior will also change. For example, the document indicates that the behavior supported by the read-only state may be completely different from that supported by the read-write state.
- How to transparently change the behavior of an object according to its state at runtime? Without introducing tight coupling between object operation and state transformation?
definition
Allows an object to change its behavior when its internal state changes, so that the object appears to have modified its behavior.
understand
//problem //Network scenario enum NetworkState { Network_Open, Network_Close, Network_Connect, }; class NetworkProcessor{ NetworkState state; public: void Operation1(){ if (state == Network_Open){ //********** state = Network_Close; } else if (state == Network_Close){ //.......... state = Network_Connect; } else if (state == Network_Connect){ //$$$$$$$$$$ state = Network_Open; } } public void Operation2(){ if (state == Network_Open){ //********** state = Network_Connect; } else if (state == Network_Close){ //..... state = Network_Open; } else if (state == Network_Connect){ //$$$$$$$$$$ state = Network_Close; } } public void Operation3(){ } };
//resolvent class NetworkState{ public: NetworkState* pNext; virtual void Operation1()=0; virtual void Operation2()=0; virtual void Operation3()=0; virtual ~NetworkState(){} }; class OpenState :public NetworkState{ static NetworkState* m_instance; public: static NetworkState* getInstance(){ if (m_instance == nullptr) { m_instance = new OpenState(); } return m_instance; } void Operation1(){ //********** pNext = CloseState::getInstance(); } void Operation2(){ //.......... pNext = ConnectState::getInstance(); } void Operation3(){ //$$$$$$$$$$ pNext = OpenState::getInstance(); } }; class CloseState:public NetworkState{ } //... //Extension method class NetworkProcessor{ NetworkState* pState; public: NetworkProcessor(NetworkState* pState){ this->pState = pState; } void Operation1(){ //... pState->Operation1(); pState = pState->pNext; //... } void Operation2(){ //... pState->Operation2(); pState = pState->pNext; //... } void Operation3(){ //... pState->Operation3(); pState = pState->pNext; //... } };
- State mode puts all behaviors related to a specific state into a subclass object of a state. When the object state is switched, the corresponding object is switched, but the state interface is maintained at the same time, so as to realize the decoupling between specific operation and state transition
- Introducing different objects for different states makes the state transformation more explicit and ensures that there will be no state inconsistency, because the transformation is atomic - that is, either completely or not.
- If the State object has no instance variables, each context can share a State object, thus saving object overhead.
Memento memo mode
motivation
During the transformation of the state of some objects, the program may be required to trace back to the state of the object at a certain point.
If some public interfaces are used to let other objects get the state of the object, the detailed implementation of the object will be exposed.
definition
Without breaking the encapsulation, capture the internal state of an object and save the state outside the object. In this way, the object can be restored to the original saved state later.
understand
class Memento { string state; //.. public: Memento(const string & s) : state(s) {} string getState() const { return state; } void setState(const string & s) { state = s; } }; class Originator { string state; //.... public: Originator() {} Memento createMomento() { Memento m(state); return m; } void setMomento(const Memento & m) { state = m.getState(); } }; int main() { Originator orginator; //Capture object status and store in memo Memento mem = orginator.createMomento(); //... Change the originator state //Recover from memo orginator.setMomento(memento); }
- In some states, it is equivalent to taking the next snapshot
- It is a little outdated today. Modern language runtime, C# and JAVA all have considerable support for sequence objectification. Therefore, Memento mode is often implemented by using the most efficient and easy to implement serialization scheme
- The core of Memento mode is information hiding, that is, the Originator needs to hide information from the outside to maintain its encapsulation, but at the same time, it needs to keep the state to the outside
- In fact, it is not necessary to make it a big hat of "design pattern" now
State change mode
Some components often have specific data structures internally. If the client program depends on these specific data structures, it will greatly destroy the reuse of components. At this time, it is an effective solution to encapsulate these specific data structures internally and provide a unified interface externally to realize access independent of specific data.
Composite mode
motivation
In some cases, the customer code depends too much on the complex internal implementation structure of the object container. The change of the internal implementation structure of the object container (rather than the abstract interface) will cause the frequent change of the customer code, which will bring the disadvantages of code maintainability, expansibility and so on
definition
Combine objects into a tree structure to represent a "part whole" hierarchy. Composite enables users to use single objects and composite objects consistently (stably)
understand
#include <iostream> #include <list> #include <string> #include <algorithm> using namespace std; class Component { public: virtual void process() = 0; virtual ~Component(){} }; //Tree node class Composite : public Component{ string name; list<Component*> elements; public: Composite(const string & s) : name(s) {} void add(Component* element) { elements.push_back(element); } void remove(Component* element){ elements.remove(element); } void process(){ //1. process current node //2. process leaf nodes for (auto &e : elements) e->process(); //Polymorphic call } }; //Leaf node class Leaf : public Component{ string name; public: Leaf(string s) : name(s) {} void process(){ //process current node } }; void Invoke(Component & c){ //... c.process(); //... } int main() { Composite root("root"); Composite treeNode1("treeNode1"); Composite treeNode2("treeNode2"); Composite treeNode3("treeNode3"); Composite treeNode4("treeNode4"); Leaf leat1("left1"); Leaf leat2("left2"); root.add(&treeNode1); treeNode1.add(&treeNode2); treeNode2.add(&leaf1); root.add(&treeNode3); treeNode3.add(&treeNode4); treeNode4.add(&leaf2); process(root); process(leaf2); process(treeNode3); }
- Convert one to many into one to one relationship
- The abstract interface between the customer code and the storage essence has occurred since
- Reverse tracing, if the parent object has frequent traversal requirements, can significantly improve the efficiency
Iterator iterator
motivation
In the process of software construction, the internal structure of collection objects often changes differently. However, for these collection objects, we hope that without exposing their internal structure, external client code can access the elements contained therein transparently. At the same time, this transparent traversal also makes it possible for the same algorithm to operate on a variety of collection objects.
definition
Provides a way to sequentially access the elements of an aggregate object without exposing (stabilizing) the internal representation of the object
understand
template<typename T> class Iterator { public: virtual void first() = 0; virtual void next() = 0; virtual bool isDone() const = 0; virtual T& current() = 0; }; template<typename T> class MyCollection{ public: Iterator<T> GetIterator(){ //... } }; template<typename T> class CollectionIterator : public Iterator<T>{ MyCollection<T> mc; public: CollectionIterator(const MyCollection<T> & c): mc(c){ } void first() override { } void next() override { } bool isDone() const override{ } T& current() override{ } }; void MyAlgorithm() { MyCollection<int> mc; Iterator<int> iter= mc.GetIterator(); for (iter.first(); !iter.isDone(); iter.next()){ cout << iter.current() << endl; } }
The iterators defined here are somewhat outdated and have a design similar to those in the existing language standard library
Chain of Responsibility
motivation
A request may be processed by multiple objects, but each request can only have one receiver at run time. If explicitly specified, it will inevitably bring about the tight coupling between the sender and receiver of the request.
definition
Make multiple objects have the opportunity to process the request, so as to avoid the coupling relationship between the sender and receiver of the request. Connect these objects into a chain and pass requests along the chain until an object processes it.
understand
#include <iostream> #include <string> using namespace std; enum class RequestType { REQ_HANDLER1, REQ_HANDLER2, REQ_HANDLER3 }; class Reqest { string description; RequestType reqType; public: Reqest(const string & desc, RequestType type) : description(desc), reqType(type) {} RequestType getReqType() const { return reqType; } const string& getDescription() const { return description; } }; class ChainHandler{ ChainHandler *nextChain;//Polymorphic pointer void sendReqestToNextHandler(const Reqest & req) { if (nextChain != nullptr) nextChain->handle(req); } protected: virtual bool canHandleRequest(const Reqest & req) = 0; virtual void processRequest(const Reqest & req) = 0; public: ChainHandler() { nextChain = nullptr; } void setNextChain(ChainHandler *next) { nextChain = next; } void handle(const Reqest & req) { if (canHandleRequest(req)) processRequest(req); else sendReqestToNextHandler(req); } }; class Handler1 : public ChainHandler{ protected: bool canHandleRequest(const Reqest & req) override { return req.getReqType() == RequestType::REQ_HANDLER1; } void processRequest(const Reqest & req) override { cout << "Handler1 is handle reqest: " << req.getDescription() << endl; } }; class Handler2 : public ChainHandler{ protected: bool canHandleRequest(const Reqest & req) override { return req.getReqType() == RequestType::REQ_HANDLER2; } void processRequest(const Reqest & req) override { cout << "Handler2 is handle reqest: " << req.getDescription() << endl; } }; class Handler3 : public ChainHandler{ protected: bool canHandleRequest(const Reqest & req) override { return req.getReqType() == RequestType::REQ_HANDLER3; } void processRequest(const Reqest & req) override { cout << "Handler3 is handle reqest: " << req.getDescription() << endl; } }; int main(){ Handler1 h1; Handler2 h2; Handler3 h3; h1.setNextChain(&h2); h2.setNextChain(&h3); Reqest req("process task ... ", RequestType::REQ_HANDLER3); h1.handle(req); return 0; }
- "Linked list" mode is not widely used
- Object responsibility assignment is more flexible, and the processing responsibilities of requests can be dynamically added / modified at run time
Behavior change pattern
Component behavior often leads to drastic changes in the component itself. The behavior change mode decouples the component itself, so as to support the change of component behavior and realize the loose coupling between the two
Command mode
motivation
In the process of software construction, "behavior requester" and "behavior implementer" usually present a kind of "tight coupling". However, in some cases - such as the need to record, revoke and redo actions, this tight coupling that can not resist change is not appropriate.
definition
Encapsulate a request (behavior) into an object, so that you can parameterize the customer with different requests; Queue or log requests, and support revocable operations.
understand
#include <iostream> #include <vector> #include <string> using namespace std; class Command { public: virtual void execute() = 0; }; class ConcreteCommand1 : public Command { string arg; public: ConcreteCommand1(const string & a) : arg(a) {} void execute() override { cout<< "#1 process..."<<arg<<endl; } }; class ConcreteCommand2 : public Command { string arg; public: ConcreteCommand2(const string & a) : arg(a) {} void execute() override { cout<< "#2 process..."<<arg<<endl; } }; class MacroCommand : public Command { vector<Command*> commands; public: void addCommand(Command *c) { commands.push_back(c); } void execute() override { for (auto &c : commands) { c->execute(); } } }; int main() { ConcreteCommand1 command1(receiver, "Arg ###"); ConcreteCommand2 command2(receiver, "Arg $$$"); MacroCommand macro; macro.addCommand(&command1); macro.addCommand(&command2); macro.execute(); }
- The command mode is somewhat similar to the C + + function object. The former has a stricter interface specification, but has performance loss. The C + + function object signature defines the behavior interface specification, which is more flexible and has higher performance.
- There will be applications in other languages. One saying is that design pattern is to make up for the shortcomings of programming language. There are good designs in C + +, and this pattern is rarely used in scenarios using C + +
Visitor accessor mode
motivation
Due to the change of requirements, new behaviors (Methods) often need to be added in some class hierarchies. If such changes are made directly in the base class, it will bring a heavy change burden to the subclass and even destroy the original design
definition
Represents an operation that acts on each element in an object structure, so that new operations (changes) acting on these elements can be defined (Extended) without changing (stabilizing) the class of elements
understand
//problem #include <iostream> using namespace std; class Visitor; class Element { public: virtual void accept(Visitor& visitor) = 0; //First polymorphism analysis virtual ~Element(){} }; class ElementA : public Element { public: void accept(Visitor &visitor) override { visitor.visitElementA(*this); } }; class ElementB : public Element { public: void accept(Visitor &visitor) override { visitor.visitElementB(*this); //Second polymorphism analysis } }; class Visitor{ public: virtual void visitElementA(ElementA& element) = 0; virtual void visitElementB(ElementB& element) = 0; virtual ~Visitor(){} }; //================================== //Extension 1 class Visitor1 : public Visitor{ public: void visitElementA(ElementA& element) override{ cout << "Visitor1 is processing ElementA" << endl; } void visitElementB(ElementB& element) override{ cout << "Visitor1 is processing ElementB" << endl; } }; //Extension 2 class Visitor2 : public Visitor{ public: void visitElementA(ElementA& element) override{ cout << "Visitor2 is processing ElementA" << endl; } void visitElementB(ElementB& element) override{ cout << "Visitor2 is processing ElementB" << endl; } }; int main() { Visitor2 visitor; ElementB elementB; elementB.accept(visitor);// double dispatch ElementA elementA; elementA.accept(visitor); return 0; }
- Double distribution
- Disadvantages: the class hierarchy is relatively certain, but the operations in it face frequent changes
- The preconditions are relatively strict and generally used less. After use, it is necessary to strictly follow the architecture specified in the design mode
Domain rule pattern
In a specific field, although some changes are frequent, they can be abstracted into some rules. At this time, combined with a specific field, the problem is abstracted into syntax rules, so as to give a general solution in this field
Interpreter parser mode
motivation
- In the process of software construction, if the problems in a specific field are complex and similar structures continue to reappear, it will face frequent changes if it is implemented by common programming methods
- In this case, the problem in a specific field is expressed as a sentence under some grammatical rules, and then an interpreter is constructed to interpret such a sentence, so as to achieve the purpose of solving the problem.
definition
Given a language, define a representation of its grammar and define an interpreter that uses the representation to interpret sentences in the language.
//Addition and subtraction #include <iostream> #include <map> #include <stack> using namespace std; class Expression { public: virtual int interpreter(map<char, int> var)=0; virtual ~Expression(){} }; //Variable expression class VarExpression: public Expression { char key; public: VarExpression(const char& key) { this->key = key; } int interpreter(map<char, int> var) override { return var[key]; } }; //Symbolic expression class SymbolExpression : public Expression { // Operator left and right parameters protected: Expression* left; Expression* right; public: SymbolExpression( Expression* left, Expression* right): left(left),right(right){ } }; //Addition operation class AddExpression : public SymbolExpression { public: AddExpression(Expression* left, Expression* right): SymbolExpression(left,right){ } int interpreter(map<char, int> var) override { return left->interpreter(var) + right->interpreter(var); } }; //Subtraction operation class SubExpression : public SymbolExpression { public: SubExpression(Expression* left, Expression* right): SymbolExpression(left,right){ } int interpreter(map<char, int> var) override { return left->interpreter(var) - right->interpreter(var); } }; Expression* analyse(string expStr) { stack<Expression*> expStack; Expression* left = nullptr; Expression* right = nullptr; for(int i=0; i<expStr.size(); i++) { switch(expStr[i]) { case '+': // Addition operation left = expStack.top(); right = new VarExpression(expStr[++i]); expStack.push(new AddExpression(left, right)); break; case '-': // Subtraction operation left = expStack.top(); right = new VarExpression(expStr[++i]); expStack.push(new SubExpression(left, right)); break; default: // Variable expression expStack.push(new VarExpression(expStr[i])); } } Expression* expression = expStack.top(); return expression; } void release(Expression* expression){ //Free node memory of expression tree } int main(int argc, const char * argv[]) { string expStr = "a+b-c+d-e";//Simple addition and subtraction map<char, int> var; var.insert(make_pair('a',5)); var.insert(make_pair('b',2)); var.insert(make_pair('c',1)); var.insert(make_pair('d',6)); var.insert(make_pair('e',10)); Expression* expression= analyse(expStr); int result=expression->interpreter(var); cout<<result<<endl; release(expression); return 0; }
- Satisfied business scenario - "business rules change frequently, similar structures appear repeatedly, and are easy to be abstracted into syntax rules"
- The parser pattern is used to represent grammar rules, so that the grammar can be easily extended using object-oriented techniques
- The parser pattern is more suitable for simple grammar representation. For the representation of complex grammar, the parser pattern will produce a large class chromatography mechanism, which needs to turn to standard tools such as syntax analysis generator.
- Today, we should make good use of its design ideas, and the application of design mode itself is limited
Design pattern summary
-
One goal: manage change and improve reuse
-
Two methods: decomposition vs abstraction
-
Eight principles: DIP, OCP, SRP, LSP, ISP, object combination, due to class inheritance, encapsulation of change points, and interface oriented programming
-
Refactoring techniques: static - > dynamic, early binding - > late binding, inheritance - > combination, compile time dependency - > runtime dependency, tight coupling - > loose coupling
-
Design patterns now replaced by other more scientific methods: Builder, Mediator, Memento, Iterator, chain of repossibility, Command, Visitor and Interpreter
-
The most commonly used structure: (combine a pointer instead of inheritance)
class A{ B* pb; // }
-
When not to use patterns: when the code readability is very poor, when the understanding of requirements is still very shallow, when the change has not yet appeared, it is not the key dependency point of the system. When the project has no reuse value, when the project is about to be released.
-
Experience: don't use patterns for patterns, pay attention to abstract classes & interfaces, clarify the change points and stability points, and examine the dependencies. There is the separation thinking of Framwork and Application. Good design is the result of evolution
-
Growth path of design mode:
-
unsophisticated
-
Able to recognize and use
-
Energy frame design
-
Forget the pattern, only the principle. Can solve problems well.