How to do the opening and closing principle with C + +?

Posted by bulgaria_mitko on Wed, 05 Jan 2022 01:37:06 +0100

C + + language is famous for its powerful function and super complexity. Many people are still confused after learning for many years. The reason is that C + + language mainly has four programming paradigms: process oriented programming, object-oriented programming, generic programming and functional programming. Each paradigm is already very complex. If you combine them, my God, my head is bald

However, if you use it skillfully, you will feel at ease. Today, let's try to make a small example with C + +. How to use C + + to realize the opening and closing principle gracefully.

1. Demand

Now, we have a small demand: a company specializes in producing pens. The colors of pens include red, green, black and yellow. The nibs of pens are divided into large, medium and small. Now, we need a class to hold the pen information. It's easy to think of the following code:

enum class Color {
	Green,
	Red,
	Yellow,
	Black
};

enum class TipSize {
	Small,
	Middle,
	Large
};

struct Pen {
	string name;
	Color color;
	TipSize tipSize;
};

Now, if we want to select a pen with a specific color from all pens, we can add the following code:

class PenFilter
{
public:
	typedef vector<Pen *> Items;

	Items by_color(Items items, Color color);
};

PenFilter::Items PenFilter::by_color(Items items, Color color) {
	Items result;
	for (auto& i : items)
		if (i->color == color)
			result.push_back(i);
	return result;
}

The code runs very well. One day, the boss came and said that we need to select some products according to the size of the nib.

That's easy. Keep adding code

PenFilter::Items PenFilter::by_tipSize(Items items, TipSize tipSize) {
	Items result;
	for (auto& i : items)
		if (i->tipSize == tipSize)
			result.push_back(i);
	return result;
}

Implementation is also easy. It is basically the same as the code for selecting according to color. However, we already smell some bad smell of code - our code is repeating.

A few days later, the boss asked to select products according to the color and the size of the pen tip at the same time. Then continue to add code.

PenFilter::Items PenFilter::by_color_and_tipSize(Items items, Color color, TipSize tipSize) {
	Items result;
	for (auto& i : items)
		if (i->color == color && i->tipSize == tipSize)
			result.push_back(i);
	return result;
}

ctrl + c, ctrl + v, and then make a little modification to get it done.

At this point, you may feel that the code is a little bad. You need to constantly modify the written classes, and a large part of the added code is the same. While there is not much code, the architecture of the code needs to be modified. In the foreseeable future, there will certainly be other different selection conditions.

2. Reconstruction

After carefully analyzing the code selected above, it is not difficult to find that the selection process is actually the same, but the rules are different. Therefore, we divide the code into two parts: rules and selection process.

Implement the base class of the rule first:

template <typename T>
class Rule
{
public:
	virtual bool passed(T* item) = 0;
};

Then select the base class:

template <typename T>
class Filter
{
public:
	virtual vector<T*> filter(vector<T*> items, Rule<T>& rule) = 0;
};

According to the base class, we implement a practical class:

class BetterFilter : Filter<Pen>
{
public:
	vector<Pen *> filter(vector < Pen * > items, Rule<Pen>& rule)override {
		vector<Pen*> result;
		for (auto& p : items)
			if (rule.passed(p))
				result.push_back(p);
		return result;
	}
 };

Similarly, let's implement classes with different rules:

class ColorRule : public Rule<Pen>
{
private:
	Color color;
public:
	explicit ColorRule(const Color color) : color(color) {}
	bool passed(Pen* item)override {
		return item->color == color;
	}
};
class TipSizeRule : public Rule<Pen>
{
private:
	TipSize tipSize;
public:
	explicit TipSizeRule(const TipSize tipSize) : tipSize(tipSize){}
	bool passed(Pen* item)override {
		return item->tipSize == tipSize;
	}
};

Here, we can carry out our test.

void ocp_test() {
	Pen pen1{ "type1", Color::Red, TipSize::Small };
	Pen pen2{ "type2", Color::Red, TipSize::Middle };
	Pen pen3{ "type3", Color::Red, TipSize::Large };
	Pen pen4{ "type4", Color::Black, TipSize::Small };
	Pen pen5{ "type5", Color::Black, TipSize::Middle };
	Pen pen6{ "type6", Color::Black, TipSize::Large };
	Pen pen7{ "type7", Color::Green, TipSize::Small };
	Pen pen8{ "type8", Color::Green, TipSize::Middle };
	Pen pen9{ "type9", Color::Green, TipSize::Large };
	Pen pen10{ "type10", Color::Yellow, TipSize::Small };
	Pen pen11{ "type11", Color::Yellow, TipSize::Middle };
	Pen pen12{ "type12", Color::Yellow, TipSize::Large };

	vector<Pen*> all{ &pen1, &pen2, &pen3, &pen4, &pen5, &pen6, 
		&pen7, &pen8, &pen9, &pen10, &pen11, &pen12 };

	BetterFilter bf;
	ColorRule  green(Color::Green);

	auto green_pens = bf.filter(all, green);
	for (auto& x : green_pens)
		cout << x->name << " is green." << endl;
}

3. Further optimization

How do we implement the union rule? For example, we need to choose a green pen with a large pen tip at the same time?

template<typename T>
class UnionRule : public Rule<T>
{
private:
	Rule<T>& first;
	Rule<T>& second;
public:
	UnionRule(Rule<T>& first, Rule<T>& second):first(first),second(second){}
	bool passed(T* item) override {
		return first.passed(item) && second.passed(item);
	}
};

Then, the test code can be modified to:

	TipSizeRule large(TipSize::Large);
	ColorRule	green(Color::Green);
	UnionRule<Pen> large_and_green(large, green);
	
	auto large_and_green_pens = bf.filter(all, large_and_green);
	for (auto& x : large_and_green_pens)
		cout << x->name << " is green and large TipSize." << endl;

In this way, the selector of union can be realized. But I still feel a little troublesome when using. Can I make the union selector more convenient to implement? Like large & & green? Really. We further modify the base class of Rule:

template <typename T>
class Rule
{
public:
	virtual bool passed(T* item) = 0;

	UnionRule<T> operator&&(Rule<T>&& other) {
		return UnionRule<T>(*this, other);
	}
};

Seeing here, I believe many people will be surrounded. UnionRule inherits from the Rule class and is now used directly in the Rule class. Is this chicken laying eggs or egg laying chicken? Moreover, if you are not familiar with the C + + compilation process, the code can easily fail to compile. It's hard to find a lot of mistakes.

4. Compilation process

In fact, when the compiler compiles the base class Rule and sees the UnionRule, the compiler does not need to know that it inherits the Rule. You just need to know that UnionRule is a class. It doesn't matter how the UnionRule class is implemented. Why? If we test a class with sizeof operator, we will find that the size of this class is the sum of the sizes of all attributes (alignment needs to be considered), and the functions in the class are shared by all classes. Moreover, the member function of the class actually hides a pointer to the object of the class. When the member function is called, it implicitly passes a this pointer.

The compiler compiles all the properties in the class before compiling the functions in the class. Therefore, the UnionRule class has been implemented when operator & & compiles in the above Rule class. Therefore, the compilation can pass and run normally.

5. Conclusion

The above code well implements the opening and closing principle. Closed for modification and open for extension.

When doing projects, we often hear that demand research is very important. This needs to be combined with our opening and closing principles today. Demand research does not ask all questions. Because the customer may not be able to answer at all. Moreover, after using the system, the customer will certainly put forward additional requirements. Didn't the previous demand survey be done in vain? Of course not. In fact, the key to demand survey is to find out which parts will not change and which parts will change! This is the core of our architecture design. If you write down a part that often needs to change, it will not be a successful architecture. Later, the higher the cost of modification.

Today's small example of opening and closing principle is introduced here. Source code can be back in the official account back to back: open and close principle source code, get. If you have other comments and suggestions, please welcome the official account.

Topics: C++ Design Pattern