命令模式
考虑一个简单的赋值语句, 例如, meaning_of_life = 42. 这个变量被赋值了, 但是却没有办法来记录何时, 何地发生的赋值, 我们也无法得知它以前的值是什么. 这样可能是有问题的, 如果没有变更记录, 我们就无法回滚到以前的值, 无法完成审计, 或基于历史的调试.
而命令模式的建议是不要直接使用对象的API来操作它们, 而是发过去一个命令对象, 告诉它该如何做. 一个Command就是一个数据类, 它的成员函数描述了要做什么和如何做.
14.1 场景我们考虑对银行账户进行建模. 账户又一个余额属性(balance)和一个透支限额(overdraft limit)属性. 我们实现两个方法: deposit() 和withdraw().
12345678910111213141516171819struct BankAccount{ int balance = 0; int overfraft_limit = -500; void deposit(int amount) { balance += amount; ...
Maybe,Monad
C++有多种方式来表示某个”对象”是否有值:
使用nullptr来表示没有值
使用智能指针(例如, shared_ptr). 它提供了检测手段
std::optional<T>是一种库解决方案. 它可以存储T值或std::nullopt.
如果我们打算使用nullptr的方法, 假定我们的领域对象定义了一个Person对象, 它可能有一个Address成员属性, 而后者有一个可选的house_name属性.
至少在英国, 有些房子是有名字的. 例如, 如果你买一座城堡, 它的地址可能不是”123 Londo Road”, 而是”Montefiore Castle”, 而这也是它的地址.当然, 不是所有的house都有name.
12345678910struct Address{ string* house_name = nullptr;}struct Person{ Address* address = nullptr;}
我们关心的是, 比如, 如何安全地打印出某个人的house name, 如果它存 ...
职责链
场景考虑一个电脑游戏, 里面的每种生物都有一个名字和两个属性: 攻击力(attack)和防御力(defense):
1234567struct Creature{ string name; int attack, defense; // 构造函数和<<操作符 ...}
在游戏中, 角色可能会拿到一些装备(比如, 一把魔法剑), 或者终结能力的增强. 不管哪种情况, 他的攻击力/防御力都会发生变化. 我们用CreatureModifier来修改它的属性.
更进一步说, 可能会有多个Modifier作用到角色上, 这种情况在游戏中并不罕见. 因此, 我们需要按照他们获取装备的次序来将这些Modifier依次作用在角色上.
首先来看一个实现:
Pointer Chain1234567891011121314151617181920212223class CreatureModifier{ CreatureModifier* next{nullptr};protected: e ...
代理模式
我们在Decorator设计模式的时候看到可以使用不同的方法来增强一个对象的功能. Proxy模式也一样, 但是它的目的通常是在尽可能地保留原有API的功能的基础上提供内部的功能增强.
Proxy并不是一种同质(homogeneous)API. 因为人们建立了各种不同的proxy来实现不同的目的. 在本章中我们会选择性地介绍一些, 你可以在网络上看到更多的proxy.
12.1 Smart Pointers最简单直观的Proxy模式的使用是智能指针. 智能指针是对指针的封装, 它会持有引用计数, 重载某些操作, 但是, 总体来说, 会提供和普通指针一样的操作接口.
12345678910struct BandAccount{ void deposit(int amount) {...}};BandAccount *ba = new BankAccount;ba->deposit(124);auto ba2 = make_shared<BandAccount>();ba2->deposit(123); // sam ...
Flyweight
Flyweight, 有时也称为token或cookie, 是一种临时性的组件, 它起着智能引用smart reference的作用. 它通常是在当存在大量十分相似的对象时, 节省内存的一种手段.
11.1 User Names假定你有一个大型多人在线游戏, 相信会有很多人叫John Smith. 因此, 如果我们用ASCII码一个个记录他们的名字, 每个用户会消耗11个字节. 相反, 我们可以存储John Smith一次, 然后为每个人存储一个指向这个名字的指针. 这样就只需要8个字节.
更进一步, 我们还可以将John Smith再一次拆分为两部分分别保存.
1234567891011121314typedef uint32_t key;struct User{ User(const string& first_name, const string& last_name) : first_name{ add(first_name)}, last_name{ add(last_name)} ...
装饰器
9.1 场景
9.2 Dynamic Decorator
9.3 Static Decorator
9.4 Functional Decorator
假设你正在使用某个由你的同事所开发的类, 你想扩展它的某个功能. 如何在不修改代码的前提下实现这一点? 一个方法是继承.
但是, 不是所有的情况下都可以使用继承. 比如, 你无法从std::vector来派生, 因为它没有虚拟析构函数; 你同样无法从int来派生. 更重要的是, 你需要的是某些增强, 而你希望将这些增强独立出来, 因为单一职责原则
9.1 场景考虑一个称为”multiple enchancement”的例子. 你有一个称为Shape的类, 有两个派生类: ColoredShape和TransparentShape, 但是你还要考虑第三种情况: ColoredTransparentShape.
两个特性的组合, 你需要3个类; 如果是三个特性的组合, 就有7个类! 这是不可接受的.
不要忘了, 我们真正需要的是不同的形状类( Square, Circle, Regtangle等等). 因此, 为了这些特 ...
组合模式
在现实中, 对象通常都包含其他的对象. 但是, 一个对象并没有什么途径来宣称自己由其他的对象组成: 字段, 就其本身来说, 除非你定义虚getter和setter函数, 否则并不能代表是接口; 同样, 虽然你可以通过实现成员函数begin()/end()来说明对象是集合, 但是应当记住, 它实际上并没有什么真的约束: 毕竟, 你可以在begin()/end()中做任何事情.
另一种说明自己的对象是容器的做法是从另一个容器中派生, 这种方法大多数情况下是可行的–即便STL容器没有定义虚拟析构函数, 但是只要你并不打算在自己的析构函数中做什么事情, 这就不是一个问题.
那么, 组合模式的目的究竟是什么呢? 本质上, 我们的目的是为了给单一对象和一组对象提供相同的接口. 为它们定义相同的接口是一件很容易的事情, 当然我们也可以试试Duck typing机制, 例如, 定义begin()/end(). Duck typing, 在C++的世界中, 通常被认为是一种很危险的想法. 因为它依赖于某些秘密的知识而不是明确定义的接口.
在Python中, Duck typing则是一种受到鼓励 ...
结构模式
当决定对象的结构时, 我们可以采取几种方法:
继承Inheritance
组合Composition: 子对象不能脱离父对象存在. 例如, 考虑若一个对象有一个owner<T>的成员, 当对象被删除时, 成员也被销毁
聚合Aggregation: 对象可以包含其他的对象, 但是这些被包含的对象也可以独立存在. 比如一个对象的类型为T*或shared_ptr<T>的成员
现在, 一般将Composition和Aggregation同样对待.
桥接模式
7.1 the Pimpl Idiom
7.2 Bridge
7.3 总结
7.1 the Pimpl Idiom从一个简单的例子看起. 假设要设计一个Person类, 存储一些个人的信息. 除了常见的设计方式之外, 你可能还见过这样的定义:
1234567891011struct Person{ std::string name; void greet(); Person(); ~Person(); class PersonImpl; PersonImpl* impl;}
为什么要这么设计?
类PersonImpl并不是在头文件中定义的, 而是在.cpp中定义的.
12345678910struct Person::PersonImpl{ void greet(Person* p);}Person::Person() : impl( new PersonImpl){}Person::~Person() { delete impl; } ...
适配器模式
小区里面加了好几个群,找到了团购的渠道。晚上出去分东西了
6.1 场景
6.2 Adapter
6.3 Adapter Trmporaries
6.4 总结
6.1 场景考虑一个例子, 假设你已经有了一个在屏幕上画点的库, 它工作的很好. 现在, 你需要做几何图形的绘制( 线, 矩形等), 你仍然想继续使用原先的画点的库. 这样, 你就需要将几何形状对象适配(Adapt)到像素上.
首先, 定义领域对象:
12345678struct Point{ int x,y;}struct Line{ Point start, end;}
接下来对几何形状做抽象
12345struct VectorObject{ virtual std::vector<Line>::iterator begin() = 0; virtual std::vector<Line>::iterator end() = 0;}
然后可以定义具体的形状–将它们定义为向量的集合:
1234 ...