如何用C++做好开闭原则呢?

目录

1. 需求

2. 重构

3. 进一步优化

4. 编译过程

5. 结尾


C++语言以功能强大和超级复杂著称。很多人学了多年,还是会一头雾水。究其原因,是因为C++语言主要有4种编程范式:面向过程编程、面向对象编程、泛型编程、函数式编程。每种范式,已经很复杂了,再把它们糅合起来,天哪,头都秃了……

但是,如果等你运用熟练了,你就会觉得游刃有余。今天,我们一起来用C++来尝试做一个小小的例子。如何用C++优雅地实现开闭原则。

1. 需求

现在,我们有一个小小的需求:某公司是专门生产钢笔的,其中,钢笔的颜色有红色、绿色、黑色、黄色等,钢笔的笔尖分为大、中、小三种。现在,我们需要用一个类来保存钢笔的信息。很容易会想到如下的代码:

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

enum class TipSize {
	Small,
	Middle,
	Large
};

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

现在,我们想从所有的笔中,挑选特定颜色的笔,那么可以添加如下代码:

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;
}

代码运行得很好,有一天,老板过来说,我们需要根据笔尖的大小,选择一部分产品。

那也很简单,继续添加代码

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;
}

实现,也很容易。和根据颜色进行选择的代码基本是一样的。但是,我们已经闻到了代码的一些坏味道了——我们的代码在重复。

又过了几天,老板,又要求同时根据颜色和笔尖的大小选择产品,那继续添加代码吧。

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,再稍作修改,搞定。

到这里,你可能觉得代码有点不太好了,需要不断的修改已经写好的类,并且,所添加代码又很大一部分是相同的。趁着代码还不多,需要对代码的架构进行修改了。在可预见的将来,肯定还会有其他不同的选择条件要求。

2. 重构

仔细分析上面选择的代码,不难发现,选择的过程其实是一样的,只是规则不同。所以,我们把代码分成规则和选择过程两部分。

先实现规则的基类:

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

再来选择的基类:

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

根据基类,我们来实现一个实用的类:

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;
	}
 };

同样,我们来实现不同规则的类:

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;
	}
};

到这里,我们就可以进行我们的测试了。

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. 进一步优化

那我们如何来实现并集的规则呢,比如同时需要选择绿色并且笔尖大的笔呢?

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);
	}
};

那么,测试代码,可以修改为:

	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;

这样,就能实现了并集的选择器。但是在使用的时候感觉还是有点麻烦,能不能让并集选择器,更加方便的实现呢?比如large&&green这样?还真可以。我们进一步修改Rule的基类:

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

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

看到这里,相信很多人会蒙圈。UnionRule是继承自Rule类的,现在直接在Rule类中使用了。这是鸡生蛋啊,还是蛋生鸡?而且,如果你对于C++编译过程不熟悉的话,代码还很容易编译不通过。报一大堆错,找都不好找哪里出错了。

4. 编译过程

其实,在编译器编译Rule这个基类的时候,看到UnionRule,编译器并不需要知道它继承了Rule。只是需要知道UnionRule是一个类就行了,至于UnionRule类是如何实现的并不重要。为什么会这样呢?如果我们用sizeof运算符测试一个类,就会发现,这个类的大小就是所有属性的大小之和(需要考虑对齐),而类中的函数是所有的类共用的。而且类的成员函数其实隐藏了一个该类对象的指针,成员函数被调用的时候,隐含的传递了一个this指针。

编译器在编译完所有类中的属性后,才去编译类中的函数。所以,在上面Rule类中operator&&编译的时候,已经实现了UnionRule类了。所以编译能够通过,并且可以正常运行。

5. 结尾

上面的代码,很好地实现了开闭原则。对于修改封闭,对于扩展开放。

在做项目的时候,经常会听到的是:需求调研很重要。这个,就需要和我们今天的开闭原则结合起来。需求的调研,不是把所有的问题都问得面面俱到。因为,客户可能根本就回答不上来,况且,客户在使用了系统后,肯定还会提出额外的需求。那前面的需求调查不是白做了吗?当然不是。其实,需求调查,最最关键的就是找出哪些部分是不会变的,哪些部分是会变的!这就是我们在做架构设计时的核心。如果,你把一个经常要变的部分写死了,那肯定不会是一个成功的架构。越到后期,修改的成本越高。

今天的开闭原则小例子就给大家介绍到这里了。需要源码的可以在公众号后台回复:开闭原则源码,获取。如有其他意见和建议,也欢迎在公众号后台留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值