目录
一、引论
(一)重要概念
(1) 奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)
- 继承者将自身作为模板参数传递给基类。
struct Foo: SomeBase<Foo>
{
//...
};
- 目的:可以在基类的实现中访问特定类型的this指针,比如在基类内部将this指针转换成派生类类型。
- 用处举例:第2章构造器模式中,2.8节构造器模式的继承性;以及第8章组合模式中,8.3节神经网络。
(2) Mixin继承
- 类可以继承它的模板参数;
template <typename T>
struct Mixin : T
{
//...
};
- 目的:允许不同类型的分层组合,派生类可以准确地使用基类的特性;
- 用处举例:第9章装饰器模式中,9.3节静态装饰器。
(3)旧风格的静态多态
这种方法存在若干不足之处,暂不做具体介绍。
(4)概念与静态多态
使用c++20中的concept
语法,来解决上一小节中存在的不足;可自行查阅相关资料。
(5)属性
- 属性只不过是一个(通常为私有的)类成员以及一个getter和setter方法的组合;
- C++标准并未将属性纳入其中,但有一个可在大多数编译器中使用的称为
property
的非标准声明符; - 属性的setter方法会在第20章观察者模式中发挥作用。
(二)SOLID设计原则
(1)单一职责原则
- 定义:每个类只有一个职责,因此也只有一个修改该类的原因;
- 例子:对于日记“Journal”这个类,其职责是“添加记录”以及“维护日记记录”,而不是将它们写入磁盘。如果将写磁盘的功能添加到Journal类以及其他类似的类中,有关持久化方法的任何改动(例如决定将其写入云端而非磁盘)都需要在受该持久化方法影响的每一个类进行许多小改动,这种情况通常存在代码异味(code smell),表明有些东西不太正确。
//1.5.1单一职责原则
struct Journal
{
std::string title_;
std::vector<std::string> entries_;
explicit Journal(const std::string& title) : title_{title} {}
void add(const std::string& entry)
{
static int count = 1;
entries_.push_back(std::to_string(count++) + ": " + entry);
}
friend std::ostream& operator<<(std::ostream& os, const Journal& obj)
{
os << "title: " << obj.title_ << std::endl;
for(const auto &item : obj.entries_)
{
os << item << std::endl;
}
return os;
}
//存在问题的持久化做法
// void save(const std::string& filename)
// {
// std::ofstream ofs(filename);
// for(auto& s : entries_)
// {
// ofs << s << std::endl;
// }
// }
};
//单独考虑持久化功能,封装在独立的类中
struct PersistenceManager
{
static void save(const Journal& j, const std::string& filemame)
{
std::ofstream ofs(filemame);
for(auto& s : j.entries_)
{
ofs << s << std::endl;
}
}
};
现在,如果我们想修改持久化机制,则在PersistenceManager
类中更改。
违背单一职责原则的一个极端的反面模式被称为上帝对象(God Object)。指的是承担了尽可能多的职责的庞大的类,是一个极其难对付的庞大的“怪物”!
(2)开闭原则
-
定义:要求软件实体对扩展开放,对修改关闭;
-
例子:产品(或其他任何对象)过滤器
- 对于产品过滤器,从概念上将其划分为两个部分:过滤器(一个以日记的所有记录为输入并返回其中某些记录的处理过程)和规范(应用于数据元素的谓词的定义);
- 对规范接口进行非常简单的定义:
template <typename T> struct Specification { virtual bool is_satisfied(T* item) = 0; };
模板中的类型T可以是Product,也可以是其他东西,该方法对所有类型的数据都是可重用的。
3. 基于Specification<T>
定义过滤器:template <typename T> struct Filter { virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) const = 0; };
以上代码为名为filter的接口指定函数签名,该接口以所有记录和规范作为输入,并且返回符合规范的记录。
4. 改进后的过滤器的实现显得很简洁:struct BetterFilter: Filter<Product> { vector<Product*> filter(vector<Product*> items, Specification<Product>& spec) const override { vector<Product*> result; for(auto& p : items) { if(spec.is_satisfied(p)) result.push_back(p); } return result; } };
- 要实现颜色过滤器,定义一个
ColorSpecification
:
struct ColorSpecification : Specification<Product> { Color color_; explicit ColorSpecification(const Color color) : color_{color} {} bool is_satisfied(Product* item) override { return item->color == color_; } };
- 按照如下方法给一堆产品做颜色筛选:
Product app{"Apple", Color::Green, Size::Small}; Product tree{"Tree", Color::Green, Size::Large}; Product house{"House", Color::Blue, Size::Large}; vector<Product*> all{&apple, &tree, &house}; BetterFilter bf; ColorSpecification green(Color::Green); auto green_things = bf.filter(all, green); for(auto& x : green_things) cout << x->name << " is green";
- 再实现尺寸过滤器,类似地定义一个
SizeSpecification
; - 如果需要实现多条件过滤,只需定义一个规范组合器;例如对于逻辑“与”,可以定义如下:
template <typename T> struct AndSpecification : Specification<T> { Specification<T>& first_; Specification<T>& second_; AndSpecification(Specification<T>& first, Specification<T>& second) : first_{first}, second_{second} {} bool is_satisfied(T* item) override { return first_.is_satisfied(item) && second_.is_satisfied(item); } };
- 现在可以基于简单的
Specification
自由地创建组合条件,例如要过滤“绿且大”的产品:
ColorSpecification green(Color::Green); SizeSpecification large(Size::Large); AndSpecification<Product> green_and_large{large, green}; //注意这里使用了大括号,也可以使用圆括号;关于c++调用构造函数使用哪种括号,有许多细微的讲究,感兴趣可以查询资料; auto big_green_things = bf.filter(all, green_and_large);
- 总结:开闭原则地主旨是:我们不必返回到已经编写和测试地代码来修改它。我们创建了Specification和Filter,接下来地工作围绕着如何设计新地过滤器接口(不改变已有接口)来展开。
(3)里氏替换原则
- 定义:如果某个接口以基类Parent类型地对象为参数,那么它应该同等地接受子类Child类对象作为参数,并且程序不会产生任何异常。
- 例子:矩形和正方形的例子;作者认为正方形类根本不应该存在!可以设计一个能创建矩形和正方形的工厂。
(4)接口隔离原则
- 定义:将复杂的接口分离为多个单独的接口,以避免强制实现者必须实现某些他们实际上并不需要的接口。
(5)依赖倒转原则
- 高层模块不应该依赖低层模块,它们都应该依赖抽象接口;
- 抽象接口不应该依赖细节,细节应该依赖抽象接口。依赖接口或基类要优于依赖具体类型。
- 一种现代的做法是使用依赖注入:实质是使用一个库(例如Boost.DI)来满足某个特殊组件的依赖关系的需求。
第一章结束,在理解了SOLID设计原则之后,可以开始正式进入设计模式的学习之旅了!