Design patterns 23 design patterns

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

  1. Support often changes
  2. 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

  1. 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
  2. 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

  1. 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.
  2. 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

  1. Because some types inherently implement logic, they have changes in two or more dimensions
  2. 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

  1. 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.
  2. 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

  1. 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.
  2. 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

  1. 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
  2. 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

  1. 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
  2. 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

  1. 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
  2. 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

  1. 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

  2. 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

  1. 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.).
  2. 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

  1. 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
  2. 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

  1. 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.
  2. 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

  1. 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
  2. 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

  1. One goal: manage change and improve reuse

  2. Two methods: decomposition vs abstraction

  3. Eight principles: DIP, OCP, SRP, LSP, ISP, object combination, due to class inheritance, encapsulation of change points, and interface oriented programming

  4. Refactoring techniques: static - > dynamic, early binding - > late binding, inheritance - > combination, compile time dependency - > runtime dependency, tight coupling - > loose coupling

  5. Design patterns now replaced by other more scientific methods: Builder, Mediator, Memento, Iterator, chain of repossibility, Command, Visitor and Interpreter

  6. The most commonly used structure: (combine a pointer instead of inheritance)

    class A{
    	B* pb;
    	//
    }
    
  7. 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.

  8. 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

  9. Growth path of design mode:

  10. unsophisticated

  11. Able to recognize and use

  12. Energy frame design

  13. Forget the pattern, only the principle. Can solve problems well.

Tags: Design Pattern

Posted by zampu on Sun, 15 May 2022 21:36:33 +0300