The original article is included in my blog, welcome.
This article is directed at github.com/kamranahme…. Translation and notes will be combined with some personal understanding. If you find that there is an obvious misunderstanding of the place, and omissions, please comment on the correction, I would be very grateful.
Design patterns and refactoring are known as the two masters of software engineering and the crystallization of wisdom in the field of software engineering. Especially design patterns, due to their high abstraction and best practice characteristics, lead to beginners and those with little programming experience, reading this is like reading a book from god. To “read” is to take a design pattern off its pedestal and introduce its essence in a more accessible way. I university period once read a “big talk design pattern”, take the road of popular understanding, however, popular can not be inaccurate, easy to understand can not understand deviation. A miss is as good as a mile. One has to learn from others.
Explain them in the simplest way possible. — The author’s words
🚀A first look way
Soft industry’s rivers and lakes, there is a principle throughout, like kendo: DRY(don’t repeat yourself). Countless philosophers tried all kinds of ways to solve this ultimate problem. The so-called design pattern is one of the most famous solutions, whose authors have four, known as “Evil of east, poison of west…” This method is not a one-trick trick, is not a specific class, library, code, you can not include, import a sit. These methods are called “guidelines,” or if translated literally, guidelines. They may sound a little bit wishy-washy, but they are really problem-specific.
Here’s a description from Wikipedia:
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 might be a little more precise, but that’s what it means.
⚠️ carefully
- Design patterns are not silver bullets (crap, there are no silver bullets)
- No dogma, no middle two, no obsessive-compulsive. If you fall into any of these three states, remember that design patterns are designed to solve problems, not to find them.
- According to local conditions, is an angel, otherwise, is the devil.
The original author used PHP7 as an example code, and I happen to be completely ignorant of the universe’s best language, had to use the old C++ to illustrate.
Types of design patterns
- Create a type
- structured
- Behavior type
Creative design patterns
In short:
The creation pattern is the solution to how do you 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
- The factory method
- The abstract factory
- The generator
- The prototype
- The singleton
🏠Simple factory
Real cases:
You need a door to build a house. Do you put on a carpenter’s suit and start sawing wood outside your door and make a mess? Or make one from a factory.
In short:
Simple factories provide the user with an instance while hiding the specific 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(a) = 0;
virtual float GetHeight(a) = 0;
};
class WoodenDoor : public IDoor {
public:
WoodenDoor(float width, float height): m_width(width), m_height(height){}
float GetWidth(a) override { return m_width; }
float GetHeight(a) override { return m_height; }
protected:
float m_width;
float m_height;
};
class DoorFactory {
public:
static IDoor* MakeDoor(float width, float heigh)
{
return newWoodenDoor(width, heigh); }};int main(a)
{
IDoor* door = DoorFactory::MakeDoor(100.200);
std: :cout << "Width: " << door->GetWidth() << std: :endl;
std: :cout << "Height: " << door->GetHeight() << std: :endl;
}Copy the code
Time of use:
When you create an object, rather than simply copying and assigning values, and have a lot of other logic involved, you should put it in a special factory instead of repeating it every time. This is done in C++ by abstracting the logic of the new statement into a singleton or, as in the example above, into a unified static function.
Nature:
It’s an abstraction when you create an object.
🏭The factory method
Real cases:
If you’re in charge of hiring, you can’t interview for every position. Depending on the nature of the job, you will need to select and delegate different people to conduct the step-by-step interview.
In short:
A method of delegating instantiation logic to a subclass
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(a) = 0;
};
class Developer : public IInterviewer
{
public:
void askQuestions(a) override {
std: :cout << "Asking about design patterns!" << std: :endl; }};class CommunityExecutive : public IInterviewer
{
public:
void askQuestions(a) override {
std: :cout << "Asking about community building!" << std: :endl; }};class HiringManager
{
public:
void takeInterview(a) {
IInterviewer* interviewer = this->makeInterviewer();
interviewer->askQuestions();
}
protected:
virtual IInterviewer* makeInterviewer(a) = 0;
};
template <typename Interviewer>
class OtherManager : public HiringManager {
protected:
IInterviewer* makeInterviewer(a) override {
return newInterviewer(); }};int main(a)
{
HiringManager* devManager = new OtherManager<Developer>();
devManager->takeInterview();
HiringManager* marketingManager = new OtherManager<CommunityExecutive>();
marketingManager->takeInterview();
}Copy the code
Time of use:
When you have some generic processing in a class, but wait until the runtime decides which subclass to use. In other words, when the customer doesn’t know which subclass he needs.
Explanation:
- The customer doesn’t know which subclass he needs? He clearly designated the client: development director and marketing director!
It should be noted that the abstract object here is the Interview process. As a user, it only knows the interface Interview, but does not know the derivative relationship of Interview. In other words, as the recruiting director (user), he just brought in the development director, the marketing director. Then he said, “Interview.” He didn’t know what the interview would be about or what form it would take. These are the parts that are encapsulated. That’s the explanation of “I don’t know which subclass I need.”
- How is this different from a simple factory?
To be fair, the simple factory example in this article is not appropriate, but the simple factory and factory method used two examples, it is confusing. But even so, you can see the biggest difference between the two: abstract dimensions. The abstraction of a simple factory is one-dimensional, it abstracts only the interface of the “type” it is creating; The factory method abstraction, on the other hand, is two-dimensional. It abstracts not only the interface of the created type, but also the interface of the method.
In the case of a simple factory example, the customer asked for a door, didn’t care about the creation process, and actually created a wooden door. Which is ironic. What if the client wants a steel door? Well, that backfired. So in this case, there are two dimensions of abstraction. One is the abstraction of the “gate” type, and the other is the abstraction of the “gate making” method. Simple factories only do the former, but do not provide solutions to the latter, which may cause customers to suffer dumb losses. If we according to the factory method of thinking, the door factory door this matter is subdivided, wooden door to wooden door factory, iron door to iron door factory. This is no different from the factory method. The client needs to specify the entrusted object first, and does not care about how to build the door:
DoorFactory* woodenDoorFactory = new WoodenDoorFactory();
woodenDoorFactory->MakeDoor(100.200);
DoorFactory* ironDoorFactory = new IronDoorFactory();
ironDoorFactory->MakeDoor(100.200);Copy the code
This becomes the factory method. Two-dimensional: “DoorFactory” and “MakeDoor”. The former is a DoorFactory no matter who is entrusted to it; The latter is whatever it makes door, it will give me a 100 by 200 door. Customers don’t care about details, they care about results. That’s the essence of abstraction. The second example is the same. “Interview” and “HiringManager” are two-dimensional abstractions. What the client wants is to complete the Interview process, and what they want is an interviewer. As for who was hired to act as the interviewer, what kind of interview was conducted. You can’t see it in the end. The result, abstracted, is: “The interviewer completed the interview.”
🔨The abstract factory
Real cases:
And then the simple factory case. Depending on actual needs, you can obtain wooden doors from wooden door stores, iron doors from iron door stores, and PVC doors from relevant stores. In addition, you need people of different professions to help you build doors, carpenters for wooden doors, welders for iron doors. It can be seen that there is a certain corresponding dependence between doors. Wooden door – carpenter, iron door – welder, etc.
In short:
A factory of factories, a group of independently managed but interdependent factories, not concerned with the details of each.
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(a) = 0;
};
class WoodenDoor : public IDoor {
public:
void GetDescription(a) override {
std: :cout << "I am a wooden door" << std: :endl; }};class IronDoor : public IDoor {
public:
void GetDescription(a) override {
std: :cout << "I am a iron door" << std: :endl; }};class IDoorFittingExpert {
public:
virtual void GetDescription(a) = 0;
};
class Carpenter : public IDoorFittingExpert {
void GetDescription(a) override {
std: :cout << "I can only fit wooden doors" << std: :endl; }};class Welder : public IDoorFittingExpert {
void GetDescription(a) override {
std: :cout << "I can only fit iron doors" << std: :endl; }};class IDoorFactory {
public:
virtual IDoor* MakeDoor(a) = 0;
virtual IDoorFittingExpert* MakeFittingExpert(a) = 0;
};
template <typenamedoortypename DoorFittingExpert>
class DoorFactory : public IDoorFactory {
public:
IDoor* MakeDoor(a) override {
return new Door();
}
IDoorFittingExpert* MakeFittingExpert(a) override {
return newDoorFittingExpert(); }};int main(a)
{
IDoorFactory* woodenFactory = new DoorFactory<WoodenDoor, Carpenter>();
{
IDoor* door = woodenFactory->MakeDoor();
IDoorFittingExpert* expert = woodenFactory->MakeFittingExpert();
door->GetDescription();
expert->GetDescription();
}
IDoorFactory* ironFactory = newDoorFactory<IronDoor, Welder>(); { IDoor* door = ironFactory->MakeDoor(); IDoorFittingExpert* expert = ironFactory->MakeFittingExpert(); door->GetDescription(); expert->GetDescription(); }}Copy the code
Time of use:
When it comes to creating logic that is not so simple, with dependencies associated with it.
Nature:
You can still understand abstract factories in terms of dimensions. Abstract factories are one more dimension than factory methods. Let’s go over the three factories again: simple factories, which are abstractions for a “type”; The factory method is an abstraction for a type and a create method; Abstract factory is an abstraction for a set of “types” and “creation methods”, each set of types in the group corresponds to the creation method. Take the example of door making: a simple factory encapsulates the operation of “door making”, and the output is a door; The factory method encapsulates the operation of “multiple door making” and entrusts “multiple factories” to output “various doors “. Abstract factory encapsulates the operation of “making a variety of doors “, the operation of” providing a variety of professionals “, and entrusts to “several factories “, the output is” a variety of doors “, and “a variety of professionals “, and” door “corresponds to” professional personnel “.
In the example, the abstract factory provides two sets of “type-create operations “(” gate – create door” and “professional – provide professional worker “), but the number is actually infinite. You can provide n sets of correspondence like this. Then delegate to the relevant factory. This is what “factories of factories” means.
👷The generator
Real cases:
Let’s say you’re at Hardee’s and you’re trying to place an order. If you say you want a Big Hardy, they’ll give it to you in no time, no questions asked. This is an example of a simple factory. But when creating logic involves more steps, like buying a hamburger at the Subway, you might have to make more choices. What kind of bread do you want? What kind of sauce? What kind of cheese? In this case, the generator pattern is needed.
In short:
Allows you to create different styles of objects while avoiding constructor contamination. This is especially useful when the object has several flavors. Or when the process of creating an object 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.
A brief description of what “the telescoping constructor anti-pattern” is. You’ll always see constructors like this:
Burger(int size, bool cheese = true.bool peperoni = true.bool tomato = false.bool lettuce = true);Copy the code
As you can already detect, the number of arguments in constructors can quickly get out of hand, and their arguments can become increasingly difficult to understand. The list will continue to grow as more options are added in the future. This is called “the Telescoping constructor anti-pattern”.
Sample code:
#include <iostream>
class Burger {
public:
class BurgerBuilder;
void showFlavors(a) {
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(a) { burger_->cheese_ = true; return *this; }
BurgerBuilder& AddPepperoni(a) { burger_->peperoni_ = true; return *this; }
BurgerBuilder& AddLettuce(a) { burger_->lettuce_ = true; return *this; }
BurgerBuilder& AddTomato(a) { burger_->tomato_ = true; return *this; }
Burger* Build(a) { return burger_; }
private:
Burger* burger_;
};
int main(a)
{
Burger* burger = Burger::BurgerBuilder(14).AddPepperoni().AddLettuce().AddTomato().Build();
burger->showFlavors();
}Copy the code
The above code is slightly different from the original code, but the main idea of the expression, and the final usage is exactly the same (additional output function, easy to run view).
Time of use:
Used when the object has several flavors and needs to avoid constructor scaling. The difference with the factory mode application scenario is that factory mode is used when the creation process is only one step in place. If you need to do this step by step, consider using the generator pattern.
Nature:
The essence of the generator pattern is to methodize a list of arguments in a constructor. Long argument lists, in both object-oriented and functional programming, are a no-no. This mode is mainly to solve this problem. The solution to this problem in functional programming is corrification, which is essentially the same as the generator pattern.
🐑The prototype
Real cases:
Remember Dolly? This goat is a clone! Details aside, the key points are all about cloning.
In short:
Create objects based on existing objects by cloning.
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 make a copy of an existing object and adapt it to your needs. Thus eliminating the process of originality.
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(a) { std: :cout << name_ << ":" << category_ << std: :endl; }
private:
std: :string name_;
std: :string category_;
};
int main(a)
{
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();
}Copy the code
The above code uses the Sheep default copy constructor and copy assignment functions. You can also override these two functions to implement custom operations.
Time of use:
When the desired object is very similar to an existing object, or when the creation process takes more time than cloning.
Nature:
Prototype models are already embedded in various language implementations. Its core is Copy. In fact, this strategy is not limited to code architecture, when you reinstall the operating system, and installed the necessary software, generally packaged a ghost, the next time to others reinstall, a direct key ghost can, such as special needs, can be installed after adjustment. This process is actually a prototype pattern. For example, we always build the environment (scaffolding) first when developing. Now we can use Docker to build, so when you use other people’s Docker package to build, in fact, it is also the prototype mode. In this way, it is more popular.
💍The singleton
Real cases:
One mountain cannot tolerate two tigers, and one country cannot have two Kings. Big things should always be handled by the same boss. So the boss is a one-off.
In short:
Ensure that an object of a particular class can be created only 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, simple profit models are often considered anti-models and should be avoided over – use. It is similar to global variables in nature and can cause problems with over-coupling or difficult debugging. Therefore, it must be used with caution.
Sample code:
To create singletons, make sure constructors are privatized, copy constructors, and copy assignments should be disabled. Create a static variable to hold this singleton.
#include <iostream>
#include <string>
#include <cassert>
class President {
public:
static President& GetInstance(a) {
static President instance;
return instance;
}
President(const President&) = delete;
President& operator= (const President&) = delete;
private:
President() {}
};
int main(a)
{
const President& president1 = President::GetInstance();
const President& president2 = President::GetInstance();
assert(&president1 == &president2); // same address, point to same object.
}Copy the code
Structural design pattern
In short:
The structural pattern focuses on object composition, in other words, how entities call each other. There’s another explanation: the answer to “How do I build a software component?” Answers to questions.
Wikipedia:
In software engineering, structural design patterns are design patterns that ease the design by identifying a simple way to realize relationships between entities.
- The adapter
- The bridge
- composition
- decoration
- appearance
- The flyweight
- The agent
🔌The adapter
Real cases:
Three examples: 1) you need a compatible adapter to transfer photos from the camera’s memory card to your computer to ensure connection; 2) a power adapter to convert a three-pin plug to a two-pin plug; 3) a translator to convert English subtitles to Chinese for watching Hollywood movies.
In short:
The adapter pattern, which wraps an object to make it compatible when it is incompatible 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(a) {
std: :cout << "I am a Lion" << std: :endl; }};class Hunter {
public:
void Hunt(ILion& lion) { lion.Roar(); }};class WildDog
{
public:
void Bark(a) {
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(a) override {
dog_.Bark();
}
private:
WildDog& dog_;
};
int main(a)
{
WildDog dog;
WildDogAdapter dogAdapter(dog);
Hunter hunter;
hunter.Hunt(dogAdapter);
}Copy the code
Nature:
In soft engineering, many problems can be solved by adding a layer in the middle. The adapter pattern is a typical application of this strategy.
🚡The bridge
Real cases:
If you have a website, there are many different kinds of pages. At the moment you have a feature that allows the user to change the theme style. What should I do? Create multiple copies for each different page? Or do you create separate themes and load them according to user preferences? Bridging mode allows you to implement the second option.
A picture is worth a thousand words:
In short:
Bridge pattern, which prioritizes composition over inheritance. Take implementation details out of the hierarchy and separate them into another set of hierarchies.
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(a) = 0;
};
class DarkTheme : public ITheme {
public:
std: :string GetColor(a) override { return "Dark Black"; }};class LightTheme : public ITheme {
public:
std: :string GetColor(a) override { return "Off white"; }};class AquaTheme : public ITheme {
public:
std: :string GetColor(a) override { return "Light blue"; }};class IWebPage {
public:
IWebPage(ITheme& theme) : theme_(theme) {}
virtual std: :string GetContent(a) = 0;
protected:
ITheme& theme_;
};
class About : public IWebPage {
public:
About(ITheme& theme) : IWebPage(theme) {}
std: :string GetContent(a) override {
return "About page in "+ theme_.GetColor(); }};class Careers : public IWebPage {
public:
Careers(ITheme& theme) : IWebPage(theme) {}
std: :string GetContent(a) override {
return "Careers page in "+ theme_.GetColor(); }};int main(a)
{
DarkTheme darkTheme;
About about(darkTheme);
Careers careers(darkTheme);
std: :cout << about.GetContent() << std: :endl;
std: :cout << careers.GetContent() << std: :endl;
}Copy the code
Nature:
The bridge pattern is essentially a split. When I talk about the adapter pattern, I have an industry saying that “many (any) problems can be solved by adding a layer in the middle.” You can view this as a plus operation. So bridge mode is actually a subtraction operation. “The first step in the face of messy, difficult-to-maintain code is to tear it down.” How to open? The bridge pattern is about stripping out a set of independent hierarchies from one hierarchy. In this case, for example, the “Theme” is a stripped abstraction. Before the glass, a lot of pages, but there is a pattern: a lot of pages, just different styles, but the same content. How to do? The DRY principle tells us to keep a copy of the same thing and abstract out different things. This is also consistent with the ultimate goal of “high cohesion, low coupling”. Once the Theme is pulled out, the rest of the Page is more cohesive, or as you might call it, more pure, taking care of the content of the Page.
Also note the similarity between the adapter and the bridge pattern in that they both have a strongly coupled association (UML) relationship. For example, the WildDogAdapter must contain a WildDog (combined UML relation). IWebPage must contain an ITheme(also combined UML relation). From this point of view, you can see that in a bridge, the relationship takes place at the interface level, whereas an adapter simply takes place at the two class levels. This is like the relationship between simple factories and factory methods. It’s a rise in dimensions, and this extra dimension is the embodiment of the “hierarchy” in the bridge mode.
🌿composition
Real cases:
Every company is made up of its employees. Every employee has the same point: if they have salary, they need to be responsible, they may have superiors, and they may have subordinates.
In short:
The composite pattern lets customers treat individual objects in a uniform 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(a) { return name_; }
virtual float GetSalary(a) { 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(a) {
float net_salary = 0;
for (auto&& employee : employees_) {
net_salary += employee.GetSalary();
}
return net_salary;
}
private:
std: :vector<Employee> employees_;
};
int main(a)
{
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;
}Copy the code
Nature:
In my opinion, combinatorial mode is not even a mode, its core is the embodiment of polymorphism. The other core is that containers store interface types and, with polymorphism, can iteratively handle common operations.
☕decoration
Real cases:
Suppose you run a car service shop that offers a variety of services. How do you calculate the charge bill? It is common to dynamically update the total price of a service while selecting a service. Here, every service is a decorator.
In short:
The decorator pattern wraps an object in a decorator class object to dynamically change the behavior of the object at run time.
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(a) = 0;
virtual std: :string GetDescription(a) = 0;
};
class SimpleCoffee : public ICoffee {
public:
float GetCost(a) override { return 10; }
std: :string GetDescription(a) override { return "Simple coffee"; }};class CoffeePlus : public ICoffee {
public:
CoffeePlus(ICoffee& coffee): coffee_(coffee) {}
virtual float GetCost(a) = 0;
virtual std: :string GetDescription(a) = 0;
protected:
ICoffee& coffee_;
};
class MilkCoffee : public CoffeePlus {
public:
MilkCoffee(ICoffee& coffee): CoffeePlus(coffee) {}
float GetCost(a) override { return coffee_.GetCost() + 2; }
std: :string GetDescription(a) override { return coffee_.GetDescription() + ", milk"; }};class WhipCoffee : public CoffeePlus {
public:
WhipCoffee(ICoffee& coffee): CoffeePlus(coffee) {}
float GetCost(a) override { return coffee_.GetCost() + 5; }
std: :string GetDescription(a) override { return coffee_.GetDescription() + ", whip"; }};class VanillaCoffee : public CoffeePlus {
public:
VanillaCoffee(ICoffee& coffee): CoffeePlus(coffee) {}
float GetCost(a) override { return coffee_.GetCost() + 3; }
std: :string GetDescription(a) override { return coffee_.GetDescription() + ", vanilla"; }};int main(a)
{
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;
}Copy the code
Nature:
The pattern of decoration is interesting, like the dynamic of static language. In fact, the implementation is similar to the composite pattern, but the key point is that it depends on the object is its parent class interface. Decoration mode, as the name implies, decorates the object itself and supports repeated decoration. In the example above, you could have added milk twice. But at the end of the day, make sure the interfaces are consistent, just like your house is still your house no matter how it’s decorated.
📦appearance
Real cases:
How do I turn it on? You say, “Press the power button.” You react this way because the computer provides a super-simple interface that hides a complex set of boot operations. This simple interface, for complex operations, is the look and feel.
In short:
The facade pattern provides a simple interface to 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(a) { std: :cout << "Ouch!" << std: :endl; }
void MakeSound(a) { std: :cout << "Beep beep!" << std: :endl; }
void ShowLoadingScreen(a) { std: :cout << "Loading..." << std: :endl; }
void Bam(a) { std: :cout << "Ready to be used!" << std: :endl; }
void CloseEverything(a) { std: :cout << "Bup bup bup buzzzz!" << std: :endl; }
void Sooth(a) { std: :cout << "Zzzzz" << std: :endl; }
void PullCurrent(a) { std: :cout << "Haaah!" << std: :endl; }};class ComputerFacade {
public:
ComputerFacade(Computer& computer): computer_(computer) {}
void TurnOn(a) {
computer_.GetElectricShock();
computer_.MakeSound();
computer_.ShowLoadingScreen();
computer_.Bam();
}
void TurnOff(a) {
computer_.CloseEverything();
computer_.PullCurrent();
computer_.Sooth();
}
private:
Computer& computer_;
};
int main(a)
{
Computer real_computer;
ComputerFacade computer(real_computer);
computer.TurnOn();
computer.TurnOff();
}Copy the code
Nature:
It’s also hard to call a pattern of patterns. It’s still the idea of one more layer, encapsulated. This extra layer is what’s called the “exterior”.
🍃The flyweight
Real cases:
Have you ever tasted freshly brewed tea at a stall? They often make extra cups of tea, in addition to the one you ask for, for other potential customers. To save resources, including heat, heat, etc. Enjoy yuan mode is aimed at this feature: sharing.
In short:
Shared with as many similar objects as possible, usually at the cost of minimal storage or computing cost.
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(a) {
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(a)
{
TeaMaker teaMaker;
TeaShop shop(teaMaker);
shop.TakeOrder("less sugar".1);
shop.TakeOrder("more milk".2);
shop.TakeOrder("without sugar".5);
shop.Serve();
}Copy the code
Nature:
The essence of the share mode is the most basic idea of cache, whether in the computer architecture cache or in the operating system page table, are the embodiment of this idea. In program design, realize this idea, the most commonly used data structure is hash table. As shown in the example, the simplest usage description is: if the key exists, take it directly; If not, create one. This saves space for repeated creation and redundancy.
🎱The agent
Real cases:
You must have used your access card to open the door? In fact, there are many ways to open the door, such as access card, or input security password and so on. The main function of the door is only “open” originally, and now access control system resembles the agent that together with the door, make it had more function. The following example code better illustrates this idea.
In short:
With proxy mode, one class behaves like 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(a) = 0;
virtual void Close(a) = 0;
};
class LabDoor : public IDoor {
public:
void Open(a) override { std: :cout << "Opening lab door" << std: :endl; }
void Close(a) 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(a) { door_.Close(); }
private:
IDoor& door_;
};
int main(a)
{
LabDoor labDoor;
Security securityDoor(labDoor);
securityDoor.Open("invalid");
securityDoor.Open("$ecr@t");
securityDoor.Close();
}Copy the code
Another example is the implementation of some data mappings. For example, I recently did an ODM (object data mapping) for MongoDB using this pattern: I used the magic method _call() to add a proxy to the MongoDB classes. All methods that pass through the proxy actually call the MongoDB class’s methods and return the result of their invocation. However, I added two exceptions: the find and findOne methods, when querying data, are mapped to the corresponding object and return that object. Instead of returning Cursor.
Nature:
The idea of adding a layer is still present, which is similar to both the adapter pattern and the facade pattern encountered above. The purpose of the adapter is clear: it is to adapt to the existing interface. The starting point is compatibility. The purpose of appearance mode is to simplify the tedious interface, the starting point is encapsulation; The purpose of proxy mode is to add more features, and the starting point is compatibility. However, the compatibility of the proxy mode is very different from that of the adapter. The adapter is really compatible from the interface, inheriting the same interface class and implementing the virtual methods of the parent class. Proxy mode, on the other hand, is compatible, more like a disguise, with the interface names remaining the same but not really related. For example, in the past, you used to use the door method, Open and Close, now switch to the security door, you still want to use these two methods habitually. However, the security door is only a proxy of the door, so its two methods of the same name are forged to show you, and the previous method has no interface compatibility.
The proxy pattern is widely used in API design, and its core is to accommodate user habits.
Behavioral design patterns
In short:
Focus on assigning responsibilities among objects. The biggest difference between them and structural patterns is that they not only specify structures, but also outline patterns for messaging/communication 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.
- Chain of responsibility
- The command
- The iterator
- broker
- The memo
- The observer
- The visitor
- strategy
- state
- Template method
🔗Chain of responsibility
Real cases:
Suppose you have three payment options in your account (A, B, and C). A has $100, B has $300 and C has $1000. The order of payment priority is from A to C. When you try to buy something with the price of $210, you will use the chain of responsibility to deal with it. You will first check whether mode A can handle it. If you can’t handle it, you will use B. If you still can’t handle it, you will use C. Until we find the right way. This chain of A’s, B’s and C’s, and this phenomenon is the chain of responsibility.
In short:
It helps to build a chain of objects. The request starts at one end and proceeds through the objects 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(a) { 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(a) override { return "Bank"; }};class Paypal : public Account {
public:
Paypal(float balance) : Account(balance) {}
std: :string GetClassName(a) override { return "Paypal"; }};class Bitcoin : public Account {
public:
Bitcoin(float balance) : Account(balance) {}
std: :string GetClassName(a) override { return "Bitcoin"; }};int main(a)
{
/ /! 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);
}Copy the code
Nature:
The essence of a responsibility chain is actually a singly linked list implementation of objects + iterations of singly linked lists. In the example above, our iteration is almost recursive. If we store the entire chain of responsibilities in a list or vector with a uniform Account, then use a for loop to iterate over access. It can also be called the chain of responsibility. So don’t be intimidated by the nouns, always go back to the basic ideas and the basic data structures and logic. So it can be applied flexibly.
👮The command
Real cases:
A typical example is ordering food in a restaurant. You (the customer) ask the server (the caller) to bring some food (the order), and the server briefly conveys the order to the cook (the receiver), who has the necessary knowledge and skills to make the dish. In another example, you (the customer) use the remote control (caller) to switch (command) the TV (receiver) program.
In short:
Allows you to encapsulate operations in objects. The core idea behind this model is to separate the customer from the receiver.
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(a) { std: :cout << "Bulb has been lit" << std: :endl; }
void TurnOff(a) { std: :cout << "Darkness!" << std: :endl; }};class ICommand {
public:
virtual void Execute(a) = 0;
virtual void Undo(a) = 0;
virtual void Redo(a) = 0;
};
class TurnOn : public ICommand {
public:
TurnOn(Bulb& bulb): bulb_(bulb) {}
void Execute(a) override { bulb_.TurnOn(); }
void Undo(a) override { bulb_.TurnOff(); }
void Redo(a) override { Execute(); }
private:
Bulb& bulb_;
};
class TurnOff : public ICommand {
public:
TurnOff(Bulb& bulb): bulb_(bulb) {}
void Execute(a) override { bulb_.TurnOff(); }
void Undo(a) override { bulb_.TurnOn(); }
void Redo(a) override { Execute(); }
private:
Bulb& bulb_;
};
class RemoteControl {
public:
void Submit(ICommand& command) { command.Execute(); }};int main(a)
{
Bulb bulb;
TurnOn turnOn(bulb);
TurnOff turnOff(bulb);
RemoteControl remote;
remote.Submit(turnOn);
remote.Submit(turnOff);
}Copy the code
Command mode is often used to implement trading infrastructure systems where you maintain a history while executing a series of commands. If the final command is successful, the sequence of commands will be reversed by backtracking if it is not.
Nature:
The command pattern is essentially an abstraction of the message protocol using a callback. Just as in the example given, it is the customer who makes the request, the cook who performs the request, and the server who connects the two, but the real link is the interface of the message, which is the command. Similarly, the TV remote control is a command. This command must be “simple “,” high granularity “, etc. This command is usually used with a history and backtracking. These are the three basic interfaces in the sample code: execute, undo, and redo. It is important to note that execute must be ACID and will usually be a collection of commands. If a command fails to be executed, the entire command is rolled back.
➿The iterator
Real cases:
An old radio is a good example of an iterator, which allows the user to start with any channel and browse the responding channels by clicking the “Next” or “previous” buttons. Using MP3 players and televisions, for example, you can also switch channels continuously using the “forward” and “backward” buttons. In other words, they all provide an interface to traverse channels, songs, or stations.
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(a) 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(a) { return stations_.begin(); }
Iter end(a) { return stations_.end(); }
private:
std: :vector<RadioStation> stations_;
};
int main(a)
{
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
}Copy the code
Nature:
A pattern that is not a pattern either. Iterators here are exactly the same concept as iterators in C++. I think there’s a trade-off between wrapping objects into containers that are native to the language. Never design for design’s sake. As far as the sample code is concerned, the StationList object is completely redundant and can be solved using a bare container. In practice, there is no need to over-design at all unless an agent is used for interface uniformity (StationList is a proxy class). The essence of an iterator is iteration. This is the most basic part of programming language, the so-called iterator pattern, is only the embodiment of this ring in object-oriented.
👽broker
Real cases:
The typical case is when you call someone on your cell phone, the communication between you and the other person is not delivered directly, but goes through the network operator in the middle. In this case, the network operator is the intermediary.
In short:
The mediator pattern adds a third party object (the mediator) to control the interaction between two objects (colleagues). It helps to decouple communication from each other, since 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(a) 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(a)
{
ChatRoom mediator;
User john("John Doe", mediator);
User jane("Jane Doe", mediator);
john.Send("Hi, there!");
jane.Send("Hey!");
}Copy the code
Nature:
The nature of the mediator is also an abstraction of the message protocol and also borrows the callback device. Sounds similar to command mode, and it is. The difference lies only in the adaptation of business scenarios: command mode applies to multiple message protocols and encapsulates each as an object. It is a tiered abstraction. You use multiple commands to achieve multiple communications. The mediator mode, on the other hand, is fixed to a certain message protocol, and colleagues must follow the same protocol to communicate. This one can be inherited, so it is a kind of deep abstraction. You communicate with the other party, it can be phone, email, wechat, etc., but they all play the role of intermediary.
💾The memo
Real cases:
In the case of calculators (originators), when you have done a set of calculations, the last one is saved in memory (memos). So you can always restore the action by pressing a button (owner).
In short:
The memo mode captures and stores the current state of the object, which can be easily recovered later.
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 is useful when you need something like snapshot-restore.
Sample code:
#include <iostream>
#include <string>
#include <memory>
class EditorMemento {
public:
EditorMemento(const std: :string& content): content_(content) {}
const std: :string& GetContent(a) 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(a) const { return content_; }
MementoType Save(a) { return std::make_shared<EditorMemento>(content_); }
void Restore(MementoType memento) { content_ = memento->GetContent(); }
private:
std: :string content_;
};
int main(a)
{
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;
}Copy the code
Nature:
The memo pattern, in plain English, is the objectification of caching. We usually want to cache a state by storing it in a container (such as a hash table, map, etc.) and calibrating it with a key (such as a timestamp). But when you’re trying to implement a fairly large, multi-state cache, with all sorts of save-and-fetch interluding, it becomes necessary to abstract the state as an object. In particular scenarios, the mode is not to look for trouble, but really will simplify the business logic.
😎The observer
Real cases:
A good example: Job seekers subscribe to a job Posting site and are notified when openings open up.
In short:
Define dependencies between objects so that dependent objects are notified when an object’s state changes.
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(a) 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(a)
{
JobSeeker johnDoe("John Doe");
JobSeeker janeDoe("Jane Doe");
JobPostings jobPostings;
jobPostings.Attach(johnDoe);
jobPostings.Attach(janeDoe);
jobPostings.AddJob(JobPost("Software Engineer"));
}Copy the code
Nature:
Also known as the publish-subscribe model. But whatever you call it, it’s all injection + callback. Subscribe is time for injection, publish is time for callback. Observation is time for injection, notification is time for callback. In practice, a subscription list is usually maintained, somewhat like a mailing list. When a notification is issued, each injected object is iterated over and a callback is performed.
🏃The visitor
Real cases:
Let’s say someone is going to Dubai and he only needs one way (visa, for example) to get into Dubai. Once there, you can visit anywhere in Dubai without having to apply for extra permits or go through legal issues. Just let him know of a place and he can visit. The visitor model does this by helping you add places so that you can visit as many places as possible without extra work.
In short:
The visitor pattern allows you to add more actions to 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(a) { std: :cout << "Ooh oo aa aa!" << std: :endl; }
void Accept(AnimalOperation& operation) override { operation.visitMonkey(*this); }};class Lion : public Animal {
public:
void Roar(a) { std: :cout << "Roaaar!" << std: :endl; }
void Accept(AnimalOperation& operation) override { operation.visitLion(*this); }};class Dolphin : public Animal {
public:
void Speak(a) { 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(a)
{
Monkey monkey;
Lion lion;
Dolphin dolphin;
Speak speak;
monkey.Accept(speak);
lion.Accept(speak);
dolphin.Accept(speak);
}Copy the code
Nature:
The essence is still injection-callback. A visa is an injection that allows you to call back to visit; Telling the location is an injection that allows you to call visit to the specific location. From the sample code, you can also see what the two-layer injection callback means: The first is an injection callback to the interface, which injects the animal behavior through Accept, then calls back to the various VISIT methods, which, at the same time, injects itself (now for specific objects), and then calls back to the specific animal behavior methods.
Why are you going around and around and back and forth? That’s because visitors are in maintenance mode. Imagine that the existing code already has Animal and its three derived classes, along with their respective howl methods. Now I want to use a unified interface to call these methods iteratively (imagine three objects in a vector, you can’t use different interfaces when iterating). That’s where the visitors come in. Add the Accept interface to the Animal class. Derived classes then override the interface and inject themselves accordingly. Finally, these methods are abstracted into an interface class and the corresponding VISIT methods are added. Derive the interface class and bind the specific methods one by one (that is, bind the callback). Then once we call Accept, the respective howling methods will be called back.
In summary, the visitor pattern is an abstraction of an invocation that relies on callbacks.
💡strategy
Real cases:
Take the sorting algorithm as an example. Initially we use bubble sort, but as the number of data increases, bubble sort becomes slower and slower. To solve this problem, we switched to quicksort. But while it was better for large data sets, it was quite slow for smaller ones. To deal with such a dilemma, we adopt a strategy: bubble sort for small data sets; For larger ones, use quicksort.
In short:
Policy mode allows you to switch algorithms or policies based on the 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(a)
{
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;
}Copy the code
Nature:
The essence of the policy pattern is still injection + polymorphism, inject the interface, call the corresponding method (algorithm or policy), and then choose the specific implementation according to the characteristics of polymorphism.
💢state
Real cases:
Suppose you are using the Draw program and select the Brush tool to draw. The brush changes its behavior based on the color you choose: if you choose red, it will draw in red; If blue is selected, it will become blue.
In short:
It lets you change the behavior of the class as the state changes
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 processor, for example, you can change the state of the typed words. For example, if you choose bold, all subsequent words will be bold, and similarly, if you choose italic, all 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(a)
{
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");
}Copy the code
Nature:
The nature of the state pattern is still injection + polymorphism, just like the strategy. In practice, the two models are almost the same. There are only a few minor differences:
- State mode usually caches the current state, which you can pass through
get
Method to get the state, but policy patterns usually do not provide itget
Methods. - The state mode provides
set
Method replaces state, but policy patterns typically do notset
(Although you can use assign constructor to achieve the same effect) - The policy object is usually passed as a parameter to the current object, and the state is usually created by the current object.
- Policies are specific to a particular method, whereas states are specific to the entire object.
See stackoverflow.com/que…
In general, a state is more like a set of policies. Changing the state of an object changes all of its methods. Policies are often specific to a particular algorithm.
📒Template method
Real cases:
Suppose we’re building a house, and the steps might look like this:
- To build the foundation
- Build by laying bricks or stones wall
- Building roof
- Across the floor
The sequence of these steps is fixed, that is, the roof cannot be built before the wall is built, but each step can be modified, for example, the wall can be replaced by wood or polyester or stone.
In short:
The template method defines the framework for how to execute an algorithm, but deferred 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 that helps us test, check, build, generate build reports (i.e., code coverage reports, check results reports, etc.), and deploy our application to a test server.
#include <iostream>
class Builder {
public:
void Build(a) {
Test();
Lint();
Assemble();
Deploy();
}
protected:
virtual void Test(a) = 0;
virtual void Lint(a) = 0;
virtual void Assemble(a) = 0;
virtual void Deploy(a) = 0;
};
class AndroidBuilder : public Builder {
void Test(a) override { std: :cout << "Running android tests" << std: :endl; }
void Lint(a) override { std: :cout << "Linting the android code" << std: :endl; }
void Assemble(a) override { std: :cout << "Assembling the android build" << std: :endl; }
void Deploy(a) override { std: :cout << "Deploying android build to server" << std: :endl; }};class IosBuilder : public Builder {
void Test(a) override { std: :cout << "Running ios tests" << std: :endl; }
void Lint(a) override { std: :cout << "Linting the ios code" << std: :endl; }
void Assemble(a) override { std: :cout << "Assembling the ios build" << std: :endl; }
void Deploy(a) override { std: :cout << "Deploying ios build to server" << std: :endl; }};int main(a)
{
AndroidBuilder androidBuilder;
androidBuilder.Build();
IosBuilder iosBuilder;
iosBuilder.Build();
}Copy the code
Nature:
The template method is basically the centralized embodiment of polymorphism. It simply brings all polymorphic methods together in a common interface. The core of the template approach, however, is to determine the order of the specific interface methods through this unified interface to establish the call structure. The subclasses implement the details individually, but their behavior and structure remain the same. This is like the “C++ standard “specifying the behavior of the language, which each compiler implements independently. Ultimately, as long as your code follows the C++ standard, in principle you should be able to get consistent results across compilers.
The last word
This note, or translation, was written intermittently, and even took half a month (spare time). But this is my intensive reading experiment of popular resources on the Internet. After all, there are too many resources that star or collect and then are forgotten and will accumulate dust for many years.
I wanted to re-read the design pattern because I wanted to refactor the imgProc, and the factory method implementation used by the original author had a large switch statement that was an eyesore. If I try OpenCV algorithm in the future, I will constantly add more algorithms, and each time I need to add different codes in multiple places, it will be a headache to think about, and I will even bother to maintain myself. I wanted to see how the factory method could be implemented more elegantly in C++.
In the process of understanding, I doubted whether I really understood several factories, so I found this popular article with more than ten thousand stars.
In fact, the 23 design patterns introduced in this article, andThe gang of Four bookExactly. The only difference is that in behavioral design patterns, the order is not quite consistent.So you can also think of this article as a “lite” version of Design Patterns – The Basics of Reusable Object-oriented Software.
However, in the process of intensive reading, I still found many problems, especially for its sample code, in fact, some examples are not appropriate (mentioned in the above notes). And there is obviously a strong correlation between some patterns, but each of the examples (e.g., three factories). For slightly more complex models, such as observer and visitor, the writing is too superficial and the examples are superficial. Feel the focus is not clear enough, suitable for promotion and popularity, but not chewing. I suggest you read it with the gang of Four book (I have to say, the examples in that book are of high quality).
One final point:
There are 23 design patterns, which are very scary, but there are only a few that should be mastered:
- In creation mode, focus on masteryThree factories, with singletonSimple factories are not really patterns (not listed separately in the book), the key is factory methods, abstract factories are just factories of factories. So the factory method is actually abstract
new
Then the key isnew
Both sides should be abstract interfaces that encapsulate the implementation. The singleton pattern needs to be understood first, but that is its core feature: global. This will be used heavily in real development, but ideally should be avoided. There are two left: the prototype is the copy, and the generator is the demolition constructor. -
In structural mode, the focus is on bridging, which is the magic tool of reconstruction. If the leader says, “This class is too complicated,” pull it out and “pull it out.” Then, nine times out of ten, you use bridge mode. Structural types can actually be classified according to the following basic properties:
- Add a layer: adapter, appearance, proxy
- Weight loss: bridging
- Common programming techniques: composition (that is, iterating over interfaces), hash (hash table), decoration (constantly instantiating and modifying parent members)
-
In the behavioral model, focus on the observer and the visitor. A common feature of behavioral patterns is their emphasis on both injection and callback. Especially with observers and visitors, use the peak. The former is the soul of all messaging systems, focusing on message lists and notification timing. The latter is the soul of the famous IoC(Inversion of control) and DI(dependency injection). This is also an essential skill for maintaining older systems. It focuses on how to add new features without breaking existing interfaces. The rest of the modes can be roughly classified as follows:
- Object oriented representation of basic data structures: chains of responsibility (object linked list polling), commands (ACID), iterators (iterator in C++), memos (cache state), states (state machine)
- DI: Mediator (injection mediation), observer (injection subscriber), Visitor (injection access interface), policy (injection algorithm object)
- Common programming techniques: Template approach (fixed interface, subclass implementation)
Patterns in different categories may sound similar in name, but they are completely different in substance. An intermediary, for example, feels similar to an adapter or broker. But the mediator is injected into the function, and the latter two add a layer to the structure.
Highlight several key points that interviews often examine: factory, singleton, bridge, observer, and visitor. The rest are too simple to examine; Or they’re so inextricably tied to the language that it’s easy to get around. But these five, it is very exploratory, if you can count clearly, the design pattern is mastered.
For all sample code, see: github.com/pezy/Desig….
If you have any questions or find any errors, please leave a message.