The original text is included in the My blog Welcome.
This article is aimed at https://github.com/kamranahme... The translation and notes will be combined with some personal understanding. If you find obvious misunderstandings and omissions, please leave a message to correct them. Thank you very much.
Interpretation of the title: Design pattern and reconfiguration are called "Soft Work Two Heroes". They are the crystallization of wisdom in the field of software engineering. Especially design pattern, because of its highly abstract and best practice characteristics, leads to beginners and those who have insufficient programming experience. Reading this is like reading a book from heaven. The so-called "human-readable" refers design pattern to the a lt ar and introduces its essence from a more understandable angle. During the school period, I read a Book "Dahua Design Model" and took the path of easy to understand. However, the popularity can not be inaccurate, easy to understand and can not understand the deviation. The difference is millions of miles.
Explain them in the simplest way possible.
(ii) A preliminary approach
There is a principle that runs through all the time, like Kendo: DRY(don't repeat yourself). Countless sages have tried all kinds of ways to solve this ultimate problem. The so-called design pattern is one of the most famous solutions. Four of its authors are known as "Eastern Evil, Western Poison". But this method is not a formula, it is not a specific class, library, code, you. These methods are called guidelines. If they are translated literally, they are guidelines. It sounds rather empty, but they are specific to specific problems.
Here we introduce Wikipedia's description:
In software engineering, a software design pattern is a general reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. It is a description or template for how to solve a problem that can be used in many different situations.
This may be more precise, but it means that.
Be careful
- Design patterns are not silver bullets (nonsense, no silver bullets at all)
- Don't be dogmatic, sophomores or obsessive-compulsive disorder. If you fall into these three states, remember that design patterns are used to solve problems, not to find fault.
- An angel is an angel, if not a devil.
The original author uses PHP7 as the example code, but I just can't understand the best language of the universe, so I have to use the old-fashioned C++ to elaborate.
Types of Design Patterns
Creative Design Patterns
In short:
Creative patterns are solutions for how to create objects
Wikipedia:
In software engineering, creational design patterns are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation.
Simple Factory
True cases:
When building a house, you need a door. Do you start sawing wood at your door in carpenter's clothes and make a mess? Or do you make one from a factory?
In short:
Simple factories provide users with an instance and hide concrete instantiation logic.
Wikipedia:
In object-oriented programming (OOP), a factory is an object for creating other objects – formally a factory is a function or method that returns objects of a varying prototype or class from some method call, which is assumed to be "new".
Sample code:
#include <iostream> class IDoor { public: virtual float GetWidth() = 0; virtual float GetHeight() = 0; }; class WoodenDoor : public IDoor { public: WoodenDoor(float width, float height): m_width(width), m_height(height){} float GetWidth() override { return m_width; } float GetHeight() override { return m_height; } protected: float m_width; float m_height; }; class DoorFactory { public: static IDoor* MakeDoor(float width, float heigh) { return new WoodenDoor(width, heigh); } }; int main() { IDoor* door = DoorFactory::MakeDoor(100, 200); std::cout << "Width: " << door->GetWidth() << std::endl; std::cout << "Height: " << door->GetHeight() << std::endl; }
Use time:
When you create an object, it is not a simple copy assignment, involving a lot of other logic, you should put it in a special factory, rather than repeating it every time. This is embodied in C++, which is mainly to abstract the logic of the new statement into a single case, or throw it into a unified static function as the example above.
Nature:
In fact, it is a concrete embodiment of abstraction in creating objects.
Factory Method
True cases:
If you are in charge of recruitment, you can't interview for any position by yourself. Depending on the nature of the job, you need to select and delegate different people to interview step by step.
In short:
A method of delegating instantiation logic to subclasses
Wikipedia:
In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.
Sample code:
#include <iostream> class IInterviewer { public: virtual void askQuestions() = 0; }; class Developer : public IInterviewer { public: void askQuestions() override { std::cout << "Asking about design patterns!" << std::endl; } }; class CommunityExecutive : public IInterviewer { public: void askQuestions() override { std::cout << "Asking about community building!" << std::endl; } }; class HiringManager { public: void takeInterview() { IInterviewer* interviewer = this->makeInterviewer(); interviewer->askQuestions(); } protected: virtual IInterviewer* makeInterviewer() = 0; }; template <typename Interviewer> class OtherManager : public HiringManager { protected: IInterviewer* makeInterviewer() override { return new Interviewer(); } }; int main() { HiringManager* devManager = new OtherManager<Developer>(); devManager->takeInterview(); HiringManager* marketingManager = new OtherManager<CommunityExecutive>(); marketingManager->takeInterview(); }
Use time:
When there are some generic processing in a class, but it's up to the runtime to decide which subclass to implement. In other words, when the customer doesn't know which subclass he needs.
Interpretation:
- Customers don't know which subcategory they need? He clearly appointed the client: the head of development department and the head of marketing department.
Note that our abstract object here is the interview process. As a user, he only knows the interface of Interview, but not the derivation of Interview. That is to say, as a hiring manager (user), he just calls in the development manager and marketing manager. Then he says, "Interview." He doesn't know what content and form the interview will take. These are encapsulated. This is the specific explanation of the above "I don't know which subclass is needed".
- What's the difference between this and simple factories?
To be fair, the examples of simple factories are not appropriate in this article, and the two examples used by simple factories and factory methods are really disturbing and confusing. But even so, we can see the biggest difference between the two: Abstract dimension. The abstraction of simple factories is one-dimensional, it is abstract only the interface of the type created; and the abstraction of factory methods is. Two-dimensional, it abstracts not only the interface of the type created, but also the interface of the method.
Specifically, in a simple factory example, the customer wants a door instead of caring about the creation process, and finally creates a wooden door. This is quite ironic. If the customer wants an iron door, that's against his will. So in this case, there are two-dimensional abstractions. One is the abstraction of the type of "door", the other is the abstraction of the method of "door-building". Simple factory Only the former has been achieved, but the latter has not been given a solution, which may cause customers to suffer dumb losses. If we subdivide the door factory building according to the idea of factory method, wooden door to wooden door factory, iron door to iron gate factory. This is the same as the example in factory method. Customers need to specify the entrusted object first, but do not care how to build it. Door:
DoorFactory* woodenDoorFactory = new WoodenDoorFactory(); woodenDoorFactory->MakeDoor(100, 200); DoorFactory* ironDoorFactory = new IronDoorFactory(); ironDoorFactory->MakeDoor(100, 200);
This is the factory method. The so-called two-dimensional: "DoorFactory" and "MakeDoor", the former is a door factory regardless of who it is entrusted to; the latter is a door factory regardless of what it builds, it will create a 100*200 door for me anyway. Customers do not care about details, but only about the results. This makes the abstract essence. The second example is the same. "Interview" and "Hiring Manager" are two-dimensional. In abstraction, the client wants to complete the interview process and an interviewer. As for who is going to be the interviewer, what aspects of the interview are invisible in the end. Finally, the abstracted result is: "the interviewer completed the interview".
(viii) Abstract factories
True cases:
Then the simple factory case. According to actual needs, you can get wooden doors from wooden doors stores, iron doors from iron doors stores, PVC doors from related stores. In addition, you need different professionals to help you install doors, wooden doors need carpenters, iron doors need welders. It can be seen that there is a certain dependence between doors. Wooddoors-carpenters, iron doors-welders, and so on. And so on.
In short:
The factories, a group of independent but interdependent factories, do not care about their details.
Wikipedia:
The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes
Sample code:
#include <iostream> class IDoor { public: virtual void GetDescription() = 0; }; class WoodenDoor : public IDoor { public: void GetDescription() override { std::cout << "I am a wooden door" << std::endl; } }; class IronDoor : public IDoor { public: void GetDescription() override { std::cout << "I am a iron door" << std::endl; } }; class IDoorFittingExpert { public: virtual void GetDescription() = 0; }; class Carpenter : public IDoorFittingExpert { void GetDescription() override { std::cout << "I can only fit wooden doors" << std::endl; } }; class Welder : public IDoorFittingExpert { void GetDescription() override { std::cout << "I can only fit iron doors" << std::endl; } }; class IDoorFactory { public: virtual IDoor* MakeDoor() = 0; virtual IDoorFittingExpert* MakeFittingExpert() = 0; }; template <typename Door, typename DoorFittingExpert> class DoorFactory : public IDoorFactory { public: IDoor* MakeDoor() override { return new Door(); } IDoorFittingExpert* MakeFittingExpert() override { return new DoorFittingExpert(); } }; int main() { IDoorFactory* woodenFactory = new DoorFactory<WoodenDoor, Carpenter>(); { IDoor* door = woodenFactory->MakeDoor(); IDoorFittingExpert* expert = woodenFactory->MakeFittingExpert(); door->GetDescription(); expert->GetDescription(); } IDoorFactory* ironFactory = new DoorFactory<IronDoor, Welder>(); { IDoor* door = ironFactory->MakeDoor(); IDoorFittingExpert* expert = ironFactory->MakeFittingExpert(); door->GetDescription(); expert->GetDescription(); } }
Use time:
When you encounter a less simple creation logic with its associated dependencies.
Nature:
Dimensions can still be used to understand abstract factories. Abstract factories have one dimension more than factory methods. Let's go through three factories again: simple factories are abstractions for a "type"; factory methods are abstractions for a "type" and a "creation method"; Abstract factories are abstractions for a group of "types" and "creation methods", each set of types and creation methods within a group. In the case of door-building, simple factories encapsulate the operation of "door-building", and output is a kind of door; factory method encapsulates the operation of "multiple doors" and entrusts "multiple factories" with the output of "various doors". Abstract factories encapsulate the operation of "multiple doors", "provide multiple professionals" and entrust the operation to "multiple factories" with the output of "various kinds of factories". Doors, as well as various professionals, correspond one by one with professionals.
In the example, Abstract factories provide two sets of "type-creation operations" (door-building and professionals-providing professionals), which are infinite in number. You can provide n sets of corresponding relationships. Then you can delegate them to the relevant factories. This is the specific meaning of "factories of factories".
(11) Generator
True cases:
Suppose you're in Hades, and you're thinking of placing an order. If you say, "Big Hardy," they'll be able to give it to you soon, without asking more. This is a simple factory example. But when creating logic involves more steps, such as buying hamburgers in Subway, you may need to make more choices. Which bread do you want? Which sauce? In this case, the generator mode is needed.
In short:
Allows you to create objects of different styles while avoiding contamination by constructors. This is especially useful when objects have several flavors. Or when the process of creating objects involves many steps.
Wikipedia:
The builder pattern is an object creation software design pattern with the intentions of finding a solution to the telescoping constructor anti-pattern.
Simply put, "the telescoping constructor anti-pattern" (the anti-pattern of a scalable constructor). You will always see the following constructors:
Burger(int size, bool cheese = true, bool peperoni = true, bool tomato = false, bool lettuce = true);
You should have noticed that the number of parameters of a constructor can quickly get out of control and its parameter arrangement will become more and more difficult to understand. To add more options in the future, the list will continue to grow. This is called the telescoping constructor anti-pattern.
Sample code:
#include <iostream> class Burger { public: class BurgerBuilder; void showFlavors() { std::cout << size_; if (cheese_) std::cout << "-cheese"; if (peperoni_) std::cout << "-peperoni"; if (lettuce_) std::cout << "-lettuce"; if (tomato_) std::cout << "-tomato"; std::cout << std::endl; } private: Burger(int size): size_(size) {} int size_ = 7; bool cheese_ = false; bool peperoni_ = false; bool lettuce_ = false; bool tomato_ = false; }; class Burger::BurgerBuilder { public: BurgerBuilder(int size) { burger_ = new Burger(size); } BurgerBuilder& AddCheese() { burger_->cheese_ = true; return *this; } BurgerBuilder& AddPepperoni() { burger_->peperoni_ = true; return *this; } BurgerBuilder& AddLettuce() { burger_->lettuce_ = true; return *this; } BurgerBuilder& AddTomato() { burger_->tomato_ = true; return *this; } Burger* Build() { return burger_; } private: Burger* burger_; }; int main() { Burger* burger = Burger::BurgerBuilder(14).AddPepperoni().AddLettuce().AddTomato().Build(); burger->showFlavors(); }
The above code is slightly different from the original code, but the main idea expressed and the final usage are exactly the same. (Additional output functions are added to facilitate operation and viewing)
Use time:
When objects have several tastes and need to avoid scaling constructors, they are used. Unlike factory mode usage scenarios, factory mode is used when the creation process is only one step in place. If step by step is required, generator mode is considered.
Nature:
The essence of the generator pattern is to methodologize the list of parameters in the constructor. Long list of parameters, whether object-oriented or functional programming, is a big taboo. The main purpose of this pattern is to solve this problem. The solution to this problem in functional programming is: currying Its essence is the same as the generator model.
Archetype
True cases:
Do you remember Dolly? This goat is cloned! Let's not go into details, the key points are focused on cloning.
In short:
By cloning, objects are created based on existing objects.
Wikipedia:
The prototype pattern is a creational design pattern in software development. It is used when the type of objects to create is determined by a prototypical instance, which is cloned to produce new objects.
In short, it allows you to copy an existing object and adapt it to your needs, thus eliminating the process of drafting.
Sample code:
#include <iostream> #include <string> class Sheep { public: Sheep(const std::string& name, const std::string& category = "Mountain Sheep") : name_(name), category_(category) {} void SetName(const std::string& name) { name_ = name; } void ShowInfo() { std::cout << name_ << " : " << category_ << std::endl; } private: std::string name_; std::string category_; }; int main() { Sheep jolly("Jolly"); jolly.ShowInfo(); Sheep dolly(jolly); // copy constructor dolly.SetName("Dolly"); dolly.ShowInfo(); Sheep doolly = jolly; // copy assignment doolly.SetName("Doolly"); doolly.ShowInfo(); }
The above code makes use of Sheep's default copy construction and copy assignment functions, which can also be rewritten to implement custom operations.
Use time:
When the required object is very similar to the existing object, or when the creation process is more time-consuming than cloning.
Nature:
The prototype model is basically embedded in various language implementations. Its core is Copy. In fact, this strategy is not limited to the code architecture. When you re-install the operating system and install the necessary software, you usually pack a ghost. Next time you re-install it to others, you can directly click ghost. If you have special needs, you can install it and then adjust it. This process is also a prototype. Patterns. For example, we always build environment (scaffolding) when we develop. Now we can use docker to build it. When you use other people's docker packages to build it, it's also a prototype model. This way of understanding, it's more popular.
Single case
True cases:
The so-called "two tigers in one mountain" and "two kings in one country" should always be handled by the same elder in major events. So this elder is a singular case.
In short:
Ensure that an object of a particular class can only be created once.
Wikipedia:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.
In fact, the single-profit model is often considered as an anti-pattern to avoid overuse. It is essentially similar to global variables, which may lead to excessive coupling and debugging difficulties. Therefore, it must be used cautiously.
Sample code:
To create a singleton, make sure that the constructor is privatized, copy constructs, copy assignments should be disabled. Create a static variable to store the singleton.
#include <iostream> #include <string> #include <cassert> class President { public: static President& GetInstance() { static President instance; return instance; } President(const President&) = delete; President& operator=(const President&) = delete; private: President() {} }; int main() { const President& president1 = President::GetInstance(); const President& president2 = President::GetInstance(); assert(&president1 == &president2); // same address, point to same object. }
Structural Design Patterns
In short:
Structural patterns focus on object composition, in other words, how entities call each other. There is another explanation: the answer to the question "How to build a software component?"
Wikipedia:
In software engineering, structural design patterns are design patterns that ease the design by identifying a simple way to realize relationships between entities.
(vi) Adapters
True cases:
Three examples: 1) You pass photos from the camera memory card to the computer, and you need an adapter compatible with this to ensure connection. 2) Power adapter, three-pin plug to two-pin plug. 3) Translate, watch Hollywood blockbusters, and translate English subtitles into Chinese.
In short:
The adapter pattern wraps an object so that it is compatible with other classes.
Wikipedia:
In software engineering, the adapter pattern is a software design pattern that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
Sample code:
#include <iostream> class ILion { public: virtual void Roar() { std::cout << "I am a Lion" << std::endl; } }; class Hunter { public: void Hunt(ILion& lion) { lion.Roar(); } }; class WildDog { public: void Bark() { std::cout << "I am a wild dog." << std::endl; } }; //! now we added a new class `WildDog`, the hunter can hunt it also. //! But we cannot do that directly because dog has a different interface. //! To make it compatible for our hunter, we will have to create an adapter. class WildDogAdapter : public ILion { public: WildDogAdapter(WildDog& dog): dog_(dog) {} void Roar() override { dog_.Bark(); } private: WildDog& dog_; }; int main() { WildDog dog; WildDogAdapter dogAdapter(dog); Hunter hunter; hunter.Hunt(dogAdapter); }
Nature:
In the field of soft engineering, many problems can be solved by adding a layer in the middle. The adapter mode is one of the typical applications of this strategy.
Bridging
True cases:
If you have a website with many different kinds of pages, and at the moment you have a function that allows users to change the theme style. What should you do? Create multiple copies of each different page? Or create separate themes and load them according to user preferences? Bridging mode allows you to implement the second solution.
A picture is worth a thousand words:
In short:
Bridging mode gives priority to combination rather than inheritance. Details of implementation are separated from hierarchy and separated into another hierarchy.
Wikipedia:
The bridge pattern is a design pattern used in software engineering that is meant to "decouple an abstraction from its implementation so that the two can vary independently"
Sample code:
#include <iostream> #include <string> class ITheme { public: virtual std::string GetColor() = 0; }; class DarkTheme : public ITheme { public: std::string GetColor() override { return "Dark Black"; } }; class LightTheme : public ITheme { public: std::string GetColor() override { return "Off white"; } }; class AquaTheme : public ITheme { public: std::string GetColor() override { return "Light blue"; } }; class IWebPage { public: IWebPage(ITheme& theme) : theme_(theme) {} virtual std::string GetContent() = 0; protected: ITheme& theme_; }; class About : public IWebPage { public: About(ITheme& theme) : IWebPage(theme) {} std::string GetContent() override { return "About page in " + theme_.GetColor(); } }; class Careers : public IWebPage { public: Careers(ITheme& theme) : IWebPage(theme) {} std::string GetContent() override { return "Careers page in " + theme_.GetColor(); } }; int main() { DarkTheme darkTheme; About about(darkTheme); Careers careers(darkTheme); std::cout << about.GetContent() << std::endl; std::cout << careers.GetContent() << std::endl; }
Nature:
Bridging mode is essentially a kind of splitting. When talking about adapter mode, I mentioned a famous line saying: "Many (any) problems can be solved by adding a layer in the middle." This can be understood as "adding" operation. Then bridging mode is actually a "subtracting" operation. In response to another sentence: "In the face of confusion, difficult to maintain code, the first step is to split." How to disassemble? Bridging mode emphasizes the separation of another independent hierarchy in one hierarchy. For example, in this case, "Theme" is an abstract concept. Before glass, there are a lot of pages, but there are rules: many pages are different in style, but the content is the same. How to do? DRY principle tells us that the same thing is retained and different things are abstracted. This is in line with the ultimate goal of "high cohesion, low coupling". After extracting Theme, the remaining Page section is more cohesive, which can also be called: more pure, only responsible for the content of the page.
Notice also the similarities between adapters and bridging patterns. They all have a strong coupling relationship (UML). For example, WildDog Adapter must contain a WildDog (combination (UML) relationship. IWebPage must contain an ITheme (also combination (UML) relationship). From this point of view, you find that in bridging, this relationship occurs at the interface level, while adapters are simple. It happens at two levels. It's like the relationship between simple factory and factory method. It's the rise of dimension, and the extra dimension is the embodiment of "hierarchy" in bridging mode.
Composition
True cases:
Every company is composed of employees. Every employee has the same points: if they have salaries, they need to be responsible, there may be superiors and subordinates.
In short:
Combination mode enables customers to treat individual objects in a unified way.
Wikipedia:
In software engineering, the composite pattern is a partitioning design pattern. The composite pattern describes that a group of objects is to be treated in the same way as a single instance of an object. The intent of a composite is to "compose" objects into tree structures to represent part-whole hierarchies. Implementing the composite pattern lets clients treat individual objects and compositions uniformly.
Sample code:
#include <iostream> #include <string> #include <vector> class Employee { public: Employee(const std::string& name, float salary): name_(name), salary_(salary) {} virtual std::string GetName() { return name_; } virtual float GetSalary() { return salary_; } protected: float salary_; std::string name_; }; class Developer : public Employee { public: Developer(const std::string& name, float salary) : Employee(name, salary) {} }; class Designer : public Employee { public: Designer(const std::string& name, float salary) : Employee(name, salary) {} }; class Organization { public: void AddEmployee(const Employee& employee) { employees_.push_back(employee); } float GetNetSalaries() { float net_salary = 0; for (auto&& employee : employees_) { net_salary += employee.GetSalary(); } return net_salary; } private: std::vector<Employee> employees_; }; int main() { Developer john("John Doe", 12000); Designer jane("Jane Doe", 15000); Organization org; org.AddEmployee(john); org.AddEmployee(jane); std::cout << "Net salaries: " << org.GetNetSalaries() << std::endl; }
Nature:
Composite mode is not even a model in my opinion. Its core is the manifestation of polymorphism. Another core is that the container stores the interface type and can handle general operations iteratively by using polymorphism.
Decorative decoration
True cases:
Suppose you run a multi-service automobile service store. How do you calculate the bills? Usually when you choose a service, the total price of the service is updated dynamically. Here, each service is a decorator.
In short:
Decoration mode wraps objects in decorative objects, which dynamically changes the behavior of the objects at runtime.
Wikipedia:
In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.
Sample code:
#include <iostream> #include <string> class ICoffee { public: virtual float GetCost() = 0; virtual std::string GetDescription() = 0; }; class SimpleCoffee : public ICoffee { public: float GetCost() override { return 10; } std::string GetDescription() override { return "Simple coffee"; } }; class CoffeePlus : public ICoffee { public: CoffeePlus(ICoffee& coffee): coffee_(coffee) {} virtual float GetCost() = 0; virtual std::string GetDescription() = 0; protected: ICoffee& coffee_; }; class MilkCoffee : public CoffeePlus { public: MilkCoffee(ICoffee& coffee): CoffeePlus(coffee) {} float GetCost() override { return coffee_.GetCost() + 2; } std::string GetDescription() override { return coffee_.GetDescription() + ", milk"; } }; class WhipCoffee : public CoffeePlus { public: WhipCoffee(ICoffee& coffee): CoffeePlus(coffee) {} float GetCost() override { return coffee_.GetCost() + 5; } std::string GetDescription() override { return coffee_.GetDescription() + ", whip"; } }; class VanillaCoffee : public CoffeePlus { public: VanillaCoffee(ICoffee& coffee): CoffeePlus(coffee) {} float GetCost() override { return coffee_.GetCost() + 3; } std::string GetDescription() override { return coffee_.GetDescription() + ", vanilla"; } }; int main() { ICoffee* someCoffee = new SimpleCoffee(); std::cout << someCoffee->GetCost() << std::endl; std::cout << someCoffee->GetDescription() << std::endl; someCoffee = new MilkCoffee(*someCoffee); std::cout << someCoffee->GetCost() << std::endl; std::cout << someCoffee->GetDescription() << std::endl; someCoffee = new WhipCoffee(*someCoffee); std::cout << someCoffee->GetCost() << std::endl; std::cout << someCoffee->GetDescription() << std::endl; someCoffee = new VanillaCoffee(*someCoffee); std::cout << someCoffee->GetCost() << std::endl; std::cout << someCoffee->GetDescription() << std::endl; }
Nature:
Decoration patterns are interesting, such as static language dynamics. They are similar in implementation to combination patterns, but the key is that they rely on their parent interface. As the name implies, decoration patterns decorate the object itself and support repeated decoration. For example, in the above example, milk can be added twice. But ultimately, the consistency of the interfaces should be guaranteed, just like your room. No matter what it looks like, it's still your house.
Appearance
True cases:
How to boot? You say "press the power button". You will react because the computer provides a super simple interface, which hides a series of complex boot operations. This simple interface, for complex operations, is the appearance.
In short:
Appearance patterns provide a simple interface for complex subsystems.
Wikipedia:
A facade is an object that provides a simplified interface to a larger body of code, such as a class library.
Sample code:
#include <iostream> class Computer { public: void GetElectricShock() { std::cout << "Ouch!" << std::endl; } void MakeSound() { std::cout << "Beep beep!" << std::endl; } void ShowLoadingScreen() { std::cout << "Loading..." << std::endl; } void Bam() { std::cout << "Ready to be used!" << std::endl; } void CloseEverything() { std::cout << "Bup bup bup buzzzz!" << std::endl; } void Sooth() { std::cout << "Zzzzz" << std::endl; } void PullCurrent() { std::cout << "Haaah!" << std::endl; } }; class ComputerFacade { public: ComputerFacade(Computer& computer): computer_(computer) {} void TurnOn() { computer_.GetElectricShock(); computer_.MakeSound(); computer_.ShowLoadingScreen(); computer_.Bam(); } void TurnOff() { computer_.CloseEverything(); computer_.PullCurrent(); computer_.Sooth(); } private: Computer& computer_; }; int main() { Computer real_computer; ComputerFacade computer(real_computer); computer.TurnOn(); computer.TurnOff(); }
Nature:
It is also very difficult to call the pattern pattern. It still uses the idea of "adding one layer" to realize it through encapsulation. The layer of "more" is called "appearance".
Xiang Yuan
True cases:
Have you ever had the experience of making instant tea at a booth? They often make more tea besides the one you ask for, and leave it to other potential customers to save resources, including heat and heat. The hedonic model aims at this feature: sharing.
In short:
Usually at the cost of minimal storage or computational cost, it is shared to as many similar objects as possible.
Wikipedia:
In computer programming, flyweight is a software design pattern. A flyweight is an object that minimizes memory use by sharing as much data as possible with other similar objects; it is a way to use objects in large numbers when a simple repeated representation would use an unacceptable amount of memory.
Sample code:
#include <iostream> #include <string> #include <unordered_map> // Anything that will be cached is flyweight. // Types of tea here will be flyweights. class KarakTea {}; class TeaMaker { public: KarakTea* Make(const std::string& preference) { if (availableTea_.find(preference) == availableTea_.end()) availableTea_.insert({preference, new KarakTea()}); return availableTea_.at(preference); } private: std::unordered_map<std::string, KarakTea*> availableTea_; }; class TeaShop { public: TeaShop(TeaMaker& teaMaker): teaMaker_(teaMaker) {} void TakeOrder(const std::string& teaType, int table) { orders_[table] = teaMaker_.Make(teaType); } void Serve() { for(auto order : orders_) std::cout << "Serving tea to table# " << order.first << std::endl; } private: std::unordered_map<int, KarakTea*> orders_; TeaMaker& teaMaker_; }; int main() { TeaMaker teaMaker; TeaShop shop(teaMaker); shop.TakeOrder("less sugar", 1); shop.TakeOrder("more milk", 2); shop.TakeOrder("without sugar", 5); shop.Serve(); }
Nature:
The essence of hedonic mode is the most basic caching idea, whether it is cache in computer architecture or page table in operating system. In programming, the most commonly used data structure to realize this idea is hash table. As shown in the example, the simplest usage description is: key exists, takes it away directly; does not exist, creates one. In order to save duplicate creation and redundant space.
Agent
True cases:
You should have used the access card to open the door? There are many ways to open the door, such as using the access card, or entering the security password, etc. The main function of the door was originally "open", but now the access control system is like an agent added to the door, so that it has more functions. The following example code will better illustrate this idea.
In short:
Using the proxy pattern, one class shows the functionality of another.
Wikipedia:
A proxy, in its most general form, is a class functioning as an interface to something else. A proxy is a wrapper or agent object that is being called by the client to access the real serving object behind the scenes. Use of the proxy can simply be forwarding to the real object, or can provide additional logic. In the proxy extra functionality can be provided, for example caching when operations on the real object are resource intensive, or checking preconditions before operations on the real object are invoked.
Sample code:
#include <iostream> #include <string> class IDoor { public: virtual void Open() = 0; virtual void Close() = 0; }; class LabDoor : public IDoor { public: void Open() override { std::cout << "Opening lab door" << std::endl; } void Close() override { std::cout << "Closing the lab door" << std::endl; } }; class Security { public: Security(IDoor& door): door_(door) {} bool Authenticate(const std::string& password) { return password == "$ecr@t"; } void Open(const std::string& password) { if (Authenticate(password)) door_.Open(); else std::cout << "Big no! It ain't possible." << std::endl; } void Close() { door_.Close(); } private: IDoor& door_; }; int main() { LabDoor labDoor; Security securityDoor(labDoor); securityDoor.Open("invalid"); securityDoor.Open("$ecr@t"); securityDoor.Close(); }
Another example is the implementation of some data mapping. For example, I recently made an ODM (Object Data Mapping) for MongoDB, using this pattern: I added a proxy to the class of MongoDB with the magic method _call(). All methods actually call the method of MongoDB class through proxy, and return the result of its call. But I added two special cases: find and findOne. When the method queries the data, it is mapped to the corresponding object and returned to it. Instead of returning to Cursor.
Nature:
It still embodies the idea of adding a layer, which is related to the above.Adapter mode andAppearance mode Here we first compare the differences between the three: adapters have a clear purpose to adapt to existing interfaces, starting from compatibility; appearance mode is to simplify tedious interfaces, starting from encapsulation; and proxy mode is to add more functions, starting from compatibility. However, the compatibility of proxy mode is quite different from that of adapters, adapters really follow. Interface compatibility, inherit the same interface class, realize the virtual method of the parent class; proxy mode is not, its compatibility is more like a camouflage, but the name of the interface remains the same, but in fact it does not matter much. For example, in the past you used the door, Open and Close methods, now you have changed to the security door, you still habitually want to use these two methods. The proxy of the door, so its two methods of the same name are actually forged to show you, and there is no interface compatibility with the previous methods.
Agent mode is widely used in API design, its core is to compatible with user habits.
Behavioral Design Patterns
In short:
Focus on the allocation of responsibilities between objects. The biggest difference between them and structural patterns is that they not only specify structures, but also outline messaging/communication patterns between structures. In other words, they answer the question of how software components behave.
Wikipadia:
In software engineering, behavioral design patterns are design patterns that identify common communication patterns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out this communication.
- Responsibility chain
- command
- iterator
- tertium quid
- Memorandum
- Observer
- Visitor
- strategy
- state
- Template method
(vii) Chain of responsibility
True cases:
Assume that you have three payment options in your account (A, B and C). But the amount is different. A has 100 dollars, B has 300 dollars and C has 1000 dollars. The priority order of payment is from A to C. When you try to buy something with a price of 210 dollars, you use the responsibility chain to deal with it. You will first see if mode A can be settled. If you can't do it, you will use mode B. If you still can't do it, you will use mode C until it is settled. Find the right way. Here's the chain of A, B and C, and this phenomenon is the chain of responsibility.
In short:
It helps to establish an object chain. Requests start at one end, access objects in turn, until the appropriate handler is found.
Wikipedia:
In object-oriented design, the chain-of-responsibility pattern is a design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain.
Sample code:
#include <iostream> #include <exception> #include <string> class Account { public: Account(float balance) : balance_(balance) {} virtual std::string GetClassName() { return "Account"; } void SetNext(Account* const account) { successor_ = account; } bool CanPay(float amount) { return balance_ >= amount; } void Pay(float amountToPay) { if (CanPay(amountToPay)) { std::cout << "Paid " << amountToPay << " using " << GetClassName() << std::endl; } else if (successor_) { std::cout << "Cannot pay using " << GetClassName() << ". Proceeding..." << std::endl; successor_->Pay(amountToPay); } else { throw "None of the accounts have enough balance."; } } protected: Account* successor_ = nullptr; float balance_; }; class Bank : public Account { public: Bank(float balance) : Account(balance) {} std::string GetClassName() override { return "Bank"; } }; class Paypal : public Account { public: Paypal(float balance) : Account(balance) {} std::string GetClassName() override { return "Paypal"; } }; class Bitcoin : public Account { public: Bitcoin(float balance) : Account(balance) {} std::string GetClassName() override { return "Bitcoin"; } }; int main() { //! Let's prepare a chain like below: //! bank -> paypal -> bitcoin //! First priority bank //! If bank can't pay then paypal //! If paypal can't pay then bit coin Bank bank(100); //> Bank with balance 100 Paypal paypal(200); //> Paypal with balance 200 Bitcoin bitcoin(300); //> Bitcoin with balance 300 bank.SetNext(&paypal); paypal.SetNext(&bitcoin); bank.Pay(259); }
Nature:
The essence of the responsibility chain is the object's single-linked list implementation + iteration of the single-linked list. In the example above, our iteration is actually close to recursive form. If we store the entire responsibility chain in a list or vector under a unified excuse, and then iterate through a for loop, it can also be called the responsibility chain. So don't be intimidated by the noun, always return to the most basic. This idea, as well as the most basic data structure and logical means, can be applied flexibly.
Command
True cases:
A typical example is ordering in a restaurant. You (customer) ask the waiter (caller) for some food (command), and then the waiter briefly conveys the command to the chef (receiver), who has the necessary knowledge and skills for cooking. Another example is that you (customer) switch TV (receiver) programs with a remote controller (caller).
In short:
Allow you to encapsulate operations in objects. The core idea behind this model is to separate customers from receivers.
Wikipadia:
In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time. This information includes the method name, the object that owns the method and values for the method parameters.
Sample code:
#include <iostream> class Bulb { public: void TurnOn() { std::cout << "Bulb has been lit" << std::endl; } void TurnOff() { std::cout << "Darkness!" << std::endl; } }; class ICommand { public: virtual void Execute() = 0; virtual void Undo() = 0; virtual void Redo() = 0; }; class TurnOn : public ICommand { public: TurnOn(Bulb& bulb): bulb_(bulb) {} void Execute() override { bulb_.TurnOn(); } void Undo() override { bulb_.TurnOff(); } void Redo() override { Execute(); } private: Bulb& bulb_; }; class TurnOff : public ICommand { public: TurnOff(Bulb& bulb): bulb_(bulb) {} void Execute() override { bulb_.TurnOff(); } void Undo() override { bulb_.TurnOn(); } void Redo() override { Execute(); } private: Bulb& bulb_; }; class RemoteControl { public: void Submit(ICommand& command) { command.Execute(); } }; int main() { Bulb bulb; TurnOn turnOn(bulb); TurnOff turnOff(bulb); RemoteControl remote; remote.Submit(turnOn); remote.Submit(turnOff); }
Command mode is usually used to implement the transaction infrastructure system. When you execute a series of commands, you maintain a historical record. If the final command is executed successfully, it will be revoked by historical backtracking. (This is more like an atomic command execution process, see ACID)
Nature:
Command mode is essentially an abstraction of message protocol, and callback is used. Just as in the example cited, the requests are made by the customer, the execution is requested by the cook, and the connection between the two is the server, but the real connector is the interface of the message, which is the command. Similarly, the TV remote controller is also the command. This command must have the characteristics of "simple" and "high granularity". It is noteworthy that execute must be ACID, usually a set of commands. Once a command fails to execute, it rolls back as a whole.
Iterator
True cases:
An old-fashioned radio is a good example of an iterator. Users can browse the response channel from any channel by clicking on the next or last button. MP3 players and TV sets can also be used as examples. You can also switch channels continuously by using the forward and backward buttons. In other words, they all provide an interface to traverse channels and songs. Or radio.
In short:
It presents a way to access object elements without exposing the underlying methods.
Wikipadia:
In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled.
Sample code:
#include <iostream> #include <vector> #include <algorithm> class RadioStation { friend bool operator==(const RadioStation& lhs, const RadioStation& rhs) { return lhs.frequency_ == rhs.frequency_; } public: RadioStation(float frequency): frequency_(frequency) {} float GetFrequency() const { return frequency_; } private: float frequency_; }; class StationList { using Iter = std::vector<RadioStation>::iterator; public: void AddStation(const RadioStation& station) { stations_.push_back(station); } void RemoveStation(const RadioStation& toRemove) { auto found = std::find(stations_.begin(), stations_.end(), toRemove); if (found != stations_.end()) stations_.erase(found); } Iter begin() { return stations_.begin(); } Iter end() { return stations_.end(); } private: std::vector<RadioStation> stations_; }; int main() { StationList stationList; stationList.AddStation(RadioStation(89)); stationList.AddStation(RadioStation(101)); stationList.AddStation(RadioStation(102)); stationList.AddStation(RadioStation(103.2)); for (auto&& station : stationList) std::cout << station.GetFrequency() << std::endl; stationList.RemoveStation(RadioStation(89)); // Will remove station 89 }
Nature:
The concept of iterator here is exactly the same as that of iterator in C++. I think it's a good choice to wrap the native container in the language as an object. Never design it for design. As far as the sample code is concerned, the StationList object is completely redundant and can be solved by naked container. In practical applications, unless it is used for the sake of unification of interfaces, it can be solved by naked container. StationList is a proxy class. Otherwise, there is no overdesign at all. The essence of iterator is iteration. This is the most basic part of programming language. The so-called iterator pattern is only the reflection of this loop in object-oriented.
(iv) Intermediaries
True cases:
Typical case is that when you call someone by mobile phone, the communication between you and the other party is not delivered directly, but through an intermediary network operator. In this case, the network operator is the intermediary.
In short:
The mediator model adds a third-party object (mediator) to control the interaction between two objects (colleagues). It helps to decouple communication between two objects. After all, they don't need to care about each other's implementation details.
Wikipedia:
In software engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program's running behavior.
Sample code:
#include <iostream> #include <string> #include <ctime> #include <iomanip> class User; class IChatRoomMediator { public: virtual void ShowMessage(const User& user, const std::string& message) = 0; }; class ChatRoom : public IChatRoomMediator { public: void ShowMessage(const User& user, const std::string& message) override; }; class User { public: User(const std::string& name, IChatRoomMediator& chatMediator): name_(name), chatMediator_(chatMediator) {} const std::string& GetName() const { return name_; } void Send(const std::string& message) { chatMediator_.ShowMessage(*this, message); } private: std::string name_; IChatRoomMediator& chatMediator_; }; void ChatRoom::ShowMessage(const User &user, const std::string &message) { std::time_t now = std::time(nullptr); std::cout << std::put_time(std::localtime(&now), "%Y-%m-%d %H:%M:%S") << "[" << user.GetName() << "]: " << message << std::endl; } int main() { ChatRoom mediator; User john("John Doe", mediator); User jane("Jane Doe", mediator); john.Send("Hi, there!"); jane.Send("Hey!"); }
Nature:
The essence of intermediaries is also an abstraction of message protocols, and also borrows the means of callbacks. Sounds likecommand The pattern is very similar, but it is also very similar. The difference lies only in the adaptation of business scenarios: command pattern applies to a variety of message protocols and encapsulates each into objects, which is a kind of flat Abstract way. You can achieve multiple communications through a variety of commands; intermediary pattern is fixed to a certain message protocol, and colleagues must follow the same protocol to achieve it. Communication. This kind of communication can be embodied by inheritance, so it is a deep abstraction. You can communicate with each other by telephone, mail, Wechat and so on, but they all play the role of intermediary.
(ix) Memorandum
True cases:
Take the calculator (primary) as an example. When you do a set of operations, the last calculation process will be saved in memory (memo). So you can restore the operation at any time through a button (person in charge).
In short:
The memo mode captures and stores the current state of the object, which can then be easily recovered.
Wikipedia:
The memento pattern is a software design pattern that provides the ability to restore an object to its previous state (undo via rollback).
This mode will be very useful when you need a snapshot-restore function like this.
Sample code:
#include <iostream> #include <string> #include <memory> class EditorMemento { public: EditorMemento(const std::string& content): content_(content) {} const std::string& GetContent() const { return content_; } private: std::string content_; }; class Editor { using MementoType = std::shared_ptr<EditorMemento>; public: void Type(const std::string& words) { content_ += " " + words; } const std::string& GetContent() const { return content_; } MementoType Save() { return std::make_shared<EditorMemento>(content_); } void Restore(MementoType memento) { content_ = memento->GetContent(); } private: std::string content_; }; int main() { Editor editor; //! Type some stuff editor.Type("This is the first sentence."); editor.Type("This is second."); //! Save the state to restore to : This is the first sentence. This is second. auto saved = editor.Save(); //! Type some more editor.Type("And this is third."); std::cout << editor.GetContent() << std::endl; editor.Restore(saved); std::cout << editor.GetContent() << std::endl; }
Nature:
Memorandum mode, to put it bluntly, is to objectify the behavior of caching. We usually want to cache a state, store it in a container (such as hash table, map, etc.) and calibrate it with a key (such as timestamp). But when you want to implement a large-scale, multi-state cache, and various "save-fetch" interpolation, abstracting the state into an object appears. It's very necessary. It's a good way to sharpen a knife and cut firewood. In a specific scenario, the model is not to find things, but to really simplify business logic.
(vii) Observers
True cases:
A good example: Job seekers subscribe to a job publishing website and are notified when a job appears.
In short:
Define the dependencies between objects so that the state of an object changes and the objects that depend on it are notified.
Wikipedia:
The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.
Sample code:
#include <iostream> #include <string> #include <vector> #include <functional> class JobPost { public: JobPost(const std::string& title): title_(title) {} const std::string& GetTitle() const { return title_; } private: std::string title_; }; class IObserver { public: virtual void OnJobPosted(const JobPost& job) = 0; }; class JobSeeker : public IObserver { public: JobSeeker(const std::string& name): name_(name) {} void OnJobPosted(const JobPost &job) override { std::cout << "Hi " << name_ << "! New job posted: " << job.GetTitle() << std::endl; } private: std::string name_; }; class IObservable { public: virtual void Attach(IObserver& observer) = 0; virtual void AddJob(const JobPost& jobPosting) = 0; protected: virtual void Notify(const JobPost& jobPosting) = 0; }; class JobPostings : public IObservable { public: void Attach(IObserver& observer) override { observers_.push_back(observer); } void AddJob(const JobPost &jobPosting) override { Notify(jobPosting); } private: void Notify(const JobPost &jobPosting) override { for (IObserver& observer : observers_) observer.OnJobPosted(jobPosting); } std::vector<std::reference_wrapper<IObserver>> observers_; }; int main() { JobSeeker johnDoe("John Doe"); JobSeeker janeDoe("Jane Doe"); JobPostings jobPostings; jobPostings.Attach(johnDoe); jobPostings.Attach(janeDoe); jobPostings.AddJob(JobPost("Software Engineer")); }
Essence:
It's also called publish-subscribe mode. Whatever it is called, it's essentially injection+callback. Subscription is injection timing, publication is callback timing. Observation is injection timing, notification is callback timing. In practice, a subscription list is usually maintained, somewhat like a mailing list. When publishing a notification, each injection object is iterated and callback is executed.
Visitors
True cases:
Assuming someone wants to go to Dubai, he only needs a way (such as a visa) to enter Dubai. When he arrives, he can visit anywhere in Dubai without additional permission or legal matters. Just let him know a place and he can visit it. Visitor mode can do this, which helps you add locations so that you don't need extra work. You can visit as many places as possible.
In short:
Visitor mode allows you to add more operations to your objects without modifying them.
Wikipedia:
In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existing object structures without modifying those structures. It is one way to follow the open/closed principle.
Sample code:
#include <iostream> class AnimalOperation; // visitee class Animal { public: virtual void Accept(AnimalOperation& operation) = 0; }; class Monkey; class Lion; class Dolphin; // visitor class AnimalOperation { public: virtual void visitMonkey(Monkey& monkey) = 0; virtual void visitLion(Lion& lion) = 0; virtual void visitDolphin(Dolphin& dolphin) = 0; }; class Monkey : public Animal { public: void Shout() { std::cout << "Ooh oo aa aa!" << std::endl; } void Accept(AnimalOperation& operation) override { operation.visitMonkey(*this); } }; class Lion : public Animal { public: void Roar() { std::cout << "Roaaar!" << std::endl; } void Accept(AnimalOperation& operation) override { operation.visitLion(*this); } }; class Dolphin : public Animal { public: void Speak() { std::cout << "Tuut tuttu tuutt!" << std::endl; } void Accept(AnimalOperation& operation) override { operation.visitDolphin(*this); } }; class Speak : public AnimalOperation { public: void visitMonkey(Monkey& monkey) override { monkey.Shout(); } void visitLion(Lion& lion) override { lion.Roar(); } void visitDolphin(Dolphin& dolphin) override { dolphin.Speak(); } }; int main() { Monkey monkey; Lion lion; Dolphin dolphin; Speak speak; monkey.Accept(speak); lion.Accept(speak); dolphin.Accept(speak); }
Essence:
In fact, the essence is still injection-callback mode. Visa is an injection that allows you to call back visit; telling location is an injection that allows you to call back visit to a specific location. From the example code, we can also see the meaning of two layers of injection callback: first, injection callback for interface, injection of animal behavior through Accept, then callback each visit method, while callback, it will also be self-contained. Body injection (now targeted) and then callback to specific animal behavior.
Why do you go around and around like this? That's because visitors are a maintenance model. Imagine that the existing code already has Animal and its three derived classes, and their respective howling methods. Now I want to use a unified interface to iteratively invoke these methods. (Imagine that all three objects are in one vector, and you can't iterate with different connections.) Then visitors are required to come on. First, add the interface Accept to the Animal class, leaving the injected mouth. Then the derived class rewrites the interface and injects itself. Finally, these methods are abstracted into an interface class and the corresponding visiting method is added. Derived from the interface class, the specific methods are bound one by one (that is, the binding callback). Then we call Acc once. Ept, the way each howls will naturally be called back.
In summary, the visitor pattern is an abstraction of callbacks, which are implemented by callbacks.
(vii) Strategy
True cases:
In order to solve this problem, we use fast sorting. Although it works well for large data sets, it is rather slow for smaller data sets. In order to deal with this dilemma, we adopt a strategy: for small data sets. For data sets, bubble sort is used; for larger ones, fast sort is used.
In short:
Policy mode allows you to switch algorithms or policies according to the actual situation.
Wikipadia:
In computer programming, the strategy pattern (also known as the policy pattern) is a behavioural software design pattern that enables an algorithm's behavior to be selected at runtime.
Sample code:
#include <iostream> #include <vector> #include <algorithm> #include <functional> #include <memory> class ISortStrategy { public: virtual void Sort(std::vector<int>& vec) = 0; }; class BubbleSortStrategy : public ISortStrategy { public: void Sort(std::vector<int>& vec) override { std::cout << "Sorting using bubble sort" << std::endl; _BubbleSort(vec); } private: void _BubbleSort(std::vector<int>& vec) { using size = std::vector<int>::size_type; for (size i = 0; i != vec.size(); ++i) for (size j = 0; j != vec.size()-1; ++j) if (vec[j] > vec[j+1]) std::swap(vec[j], vec[j+1]); } }; class QuickSortStrategy : public ISortStrategy { public: void Sort(std::vector<int>& vec) override { std::cout << "Sorting using quick sort" << std::endl; _QuickSort(vec); } private: void _QuickSort(std::vector<int>& vec) { using size = std::vector<int>::size_type; auto partition = [&vec](size low, size high) { int pivot = vec[high]; size i = low; for (size j = low; j != high; ++j) if (vec[j] <= pivot) std::swap(vec[i++], vec[j]); std::swap(vec[i], vec[high]); return i; }; std::function<void (size, size)> quickSort = [&](size low, size high) { if (low >= high) return; size pi = partition(low, high); quickSort(low, pi - 1); quickSort(pi + 1, high); }; quickSort(0, vec.size()-1); } }; class Sorter { public: static void Sort(std::vector<int>& vec, const std::shared_ptr<ISortStrategy>& sorter) { sorter->Sort(vec); } }; int main() { std::vector<int> vec{1, 5, 4, 3, 2, 8}; Sorter::Sort(vec, std::make_shared<BubbleSortStrategy>()); for (int i : vec) std::cout << i << " "; std::cout << std::endl; Sorter::Sort(vec, std::make_shared<QuickSortStrategy>()); for (int i : vec) std::cout << i << " "; std::cout << std::endl; }
Essence:
The essence of policy pattern is still injection + polymorphism. When interface is injected and the corresponding method (algorithm or policy) is called, the specific implementation is selected according to the characteristics of polymorphism.
(vii) Status
True cases:
Suppose you are using the "drawing" program and choose the brush tool to draw. The brush will change its behavior according to the color you choose: for example, if you choose red, it will draw in red; if you choose blue, it will become blue.
In short:
It allows you to change the behavior of classes as well as the state.
Wikipedia:
The state pattern is a behavioral software design pattern that implements a state machine in an object-oriented way. With the state pattern, a state machine is implemented by implementing each individual state as a derived class of the state pattern interface, and implementing state transitions by invoking methods defined by the pattern's superclass. The state pattern can be interpreted as a strategy pattern which is able to switch the current strategy through invocations of methods defined in the pattern's interface.
Sample code:
In a word processing program, for example, you can change the state of the typed words. For example, if you choose bold, the subsequent words will be bold. Similarly, if you choose italic, the subsequent words will be italic.
#include <iostream> #include <string> #include <algorithm> #include <memory> class IWritingState { public: virtual void Write(std::string words) = 0; }; class UpperCase : public IWritingState { void Write(std::string words) override { std::transform(words.begin(), words.end(), words.begin(), ::toupper); std::cout << words << std::endl; } }; class LowerCase : public IWritingState { void Write(std::string words) override { std::transform(words.begin(), words.end(), words.begin(), ::tolower); std::cout << words << std::endl; } }; class Default : public IWritingState { void Write(std::string words) override { std::cout << words << std::endl; } }; class TextEditor { public: TextEditor(const std::shared_ptr<IWritingState>& state): state_(state) {} void SetState(const std::shared_ptr<IWritingState>& state) { state_ = state; } void Type(std::string words) { state_->Write(words); } private: std::shared_ptr<IWritingState> state_; }; int main() { TextEditor editor(std::make_shared<Default>()); editor.Type("First line"); editor.SetState(std::make_shared<UpperCase>()); editor.Type("Second line"); editor.Type("Third line"); editor.SetState(std::make_shared<LowerCase>()); editor.Type("Fourth line"); editor.Type("Fifth line"); }
Essence:
The essence of state mode is still injection + polymorphism. This andstrategy In practice, the two models are almost the same. There are only slight differences:
- State mode usually caches the current state. You can get the state by get method, but policy mode usually does not provide get method.
- The state mode provides a set method to replace the state, but the policy mode usually does not provide a set method. (Although assign constructor can be used to do the same.)
- Policy objects are usually passed to the current object as parameters, and the state is usually created by the current object.
- Policies are for specific methods, while states are for the whole object.
For more information https://stackoverflow.com/que...
Generally speaking, state is more like a set of policies. Changing the state of an object can change all kinds of methods of the object. And the strategy is usually only for a specific algorithm.
(x) Template method
True cases:
Assuming that we are building a house, the steps may look as follows:
- Foundation construction
- Wall-building
- Building a roof
- Separate floors
The sequence of these steps is fixed, i.e. the roof cannot be built before the wall is built, but each step can be modified and perfected. For example, the wall can also be replaced by wood or polyester or stone.
In short:
Template method defines the framework of how to execute an algorithm, but delays the implementation to subclasses.
Wikipedia:
In software engineering, the template method pattern is a behavioral design pattern that defines the program skeleton of an algorithm in an operation, deferring some steps to subclasses. It lets one redefine certain steps of an algorithm without changing the algorithm's structure.
Sample code:
Suppose we have a build tool to help us test, check, build, generate build reports (code coverage reports, check results reports, etc.) and deploy our applications to the test server.
#include <iostream> class Builder { public: void Build() { Test(); Lint(); Assemble(); Deploy(); } protected: virtual void Test() = 0; virtual void Lint() = 0; virtual void Assemble() = 0; virtual void Deploy() = 0; }; class AndroidBuilder : public Builder { void Test() override { std::cout << "Running android tests" << std::endl; } void Lint() override { std::cout << "Linting the android code" << std::endl; } void Assemble() override { std::cout << "Assembling the android build" << std::endl; } void Deploy() override { std::cout << "Deploying android build to server" << std::endl; } }; class IosBuilder : public Builder { void Test() override { std::cout << "Running ios tests" << std::endl; } void Lint() override { std::cout << "Linting the ios code" << std::endl; } void Assemble() override { std::cout << "Assembling the ios build" << std::endl; } void Deploy() override { std::cout << "Deploying ios build to server" << std::endl; } }; int main() { AndroidBuilder androidBuilder; androidBuilder.Build(); IosBuilder iosBuilder; iosBuilder.Build(); }
Essence:
Template method is basically a centralized manifestation of polymorphism. It only concentrates all polymorphic methods into a common interface. However, the core of template method is to determine the sequence of each specific interface method through this unified interface to establish the invocation structure. Subclasses implement specific details, but the behavior and structure remain the same. This is similar to the "C++ standard" which specifies the behavior of the language. Each compiler implements it individually. Ultimately, as long as your code follows the C++ standard, you should be able to get consistent results on all compilers in principle.
Last words
It took me half a month (spare time) to finish this note intermittently. But it was an intensive reading experiment of the popular resources on the Internet. After all, there were too many resources that were forgotten after star ing or collecting, and they would accumulate dust for many years.
Suddenly I want to re-read the design pattern, because I want to reconstruct it. imgproc For the original author Realization of Factory Method In the future, I will try OpenCV algorithm and add more algorithms. Every time I need to add different codes in many places, it is a headache to think about. It is troublesome to maintain myself. Then I want to know how to implement factory more elegantly through C++ language.
In the process of understanding, I doubted whether I really understood several factories, so I found this hot article with more than ten thousand star ters.
In fact, the 23 design patterns introduced in this article are The book of the Gang of Four Complete correspondence. The only difference is that the sequence of behavioral design patterns is not very consistent. So you can also view this article as a "streamlined version" of "Design Patterns - the Foundation of Reusable Object-Oriented Software".
However, in the process of intensive reading, many problems have been found, especially for its sample code. In fact, some examples are not appropriate (mentioned in the notes above). And there are obviously strong correlations between some patterns, but each of them cites examples (such as three factories). For slightly complex patterns, such as observers and visitors, the writing is too superficial and the examples are superficial. Points are not clear enough for popularization, but they are not chewy. It is suggested to consult with the book of the Gang of Four. (I have to say, the examples in that book are of high quality.)
Finally, draw the key points:
Speaking of 23 design modes, they are very frightening. In fact, only a few of them should be mastered.
- In the model of creation, we should focus on three factories and singletons. Simple factories are not models (not listed separately in the book). The key is the factory method. The abstract factory is just the factory of the factory. So the factory method is actually the process of new. The key is that both sides of new should be abstract interfaces, encapsulating the concrete implementation. The singleton mode, we need to start with. Solve its drawbacks, but that's also its core feature: the overall situation. This thing will be widely used in actual development, but ideally should be avoided. The remaining two: the prototype is copy, and the generator is "demolition constructor".
-
In the structural mode, the emphasis is on bridging, which is to reconstruct artifacts. If the leader says that this type is too complex, he "pulls it out" and "carries it out alone". Ninety-nine out of ten are using the bridging mode. In fact, structural types can be classified according to the following basic characteristics:
- Add one layer: adapter, appearance, proxy
- Weight loss: bridging
- Common programming techniques: combination (i.e. interface-based iteration), hedonic (hash table), decoration (constantly instantiating modification of parent class members)
-
Behavioral patterns focus on observers and visitors. Behavioral patterns have a common feature, that is, they pay great attention to injection and callback. Especially for observers and visitors, the former is the soul of all message systems, focusing on message lists and timing of notification. The latter is the soul of the famous IoC (Control Inversion) and DI (Dependent Injection). This is also a necessary skill to maintain the old system. It focuses on how to add new features without destroying the existing interfaces. The remaining patterns are roughly classified as follows:
- The basic data structure embodies in object-oriented: responsibility chain (object list polling), command (ACID), iterator (iterator in C++), memo (cache state), state machine (state machine).
- DI: Mediator (Injection Mediator), Observer (Injection Subscriber), Visitor (Injection Access Interface), Policy (Injection Algorithmic Object)
- Common programming skills: template approach (fixed interface, subclass implementation)
Tips: Patterns of different categories may sound similar, but in essence they are quite different. For example, intermediaries, like adapters and proxies, feel very similar. But intermediaries are injected to work, while the latter two add a layer of structure.
Several key points are also often examined in interviews: factories, singletons, bridges, observers, visitors. The rest are either too simple to examine, or they are inseparable from linguistic characteristics and easily bypassed when asked. But these five points are very investigative, and if they can be counted clearly, the design pattern will be mastered.
For all sample code, see: https://github.com/pezy/Desig...
If you have any questions or find any mistakes, please leave a message.