假设你正在使用某个由你的同事所开发的类, 你想扩展它的某个功能. 如何在不修改代码的前提下实现这一点? 一个方法是继承.
但是, 不是所有的情况下都可以使用继承. 比如, 你无法从std::vector
来派生, 因为它没有虚拟析构函数; 你同样无法从int
来派生. 更重要的是, 你需要的是某些增强, 而你希望将这些增强独立出来, 因为单一职责原则
9.1 场景
考虑一个称为”multiple enchancement”的例子. 你有一个称为Shape
的类, 有两个派生类: ColoredShape
和TransparentShape
, 但是你还要考虑第三种情况: ColoredTransparentShape
.
两个特性的组合, 你需要3个类; 如果是三个特性的组合, 就有7个类! 这是不可接受的.
不要忘了, 我们真正需要的是不同的形状类( Square, Circle, Regtangle
等等). 因此, 为了这些特性而派生出不同的类是不合理的.
首先定义Shape
类. 这是一个抽象类:
1 2 3 4
| struct Shape { virtual string str() const = 0; }
|
接下来实现一个具体类, Circle
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct Circle: Shape { float radius; explicit Circle(const float radius) : radius(radius){} void resize(float factor){ radius *= factor; }
string str() const override { ostringstream oss; oss << "A circle os radius " << radius; return oss.str(); } }
|
使用Composition模式来实现Decorator模式, 有两种不同的方法.
Dynamic composition 在运行期组合, 通常是通过传递引用参数实现的. 它提供了最大程度的灵活性.
Static composition 在编译期实现的, 一般是通过模板实现.
9.2 Dynamic Decorator
用Composition来实现支持颜色的Shape:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct ColoredShape : Shape { Shape& shape; string color; ColoredShape(Shape& shape, const string& color) : shape{shpae}, color{color} {} string str() const override { ostringstream oss; oss << shape.str() << "has the color" << color; return oss.str(); } }
|
使用:
1 2 3 4
| Circle circle{0.5f}; ColoredShape redCircle{circle, "red"}; cout << redCircle.str(); ...
|
当我们需要另一个增强类时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct TransparentShape : Shape { Shape& shape; uint8_t transparency;
TransparentShape(Shape& shape, const int8_t transparency) : shape{shape}, transparency{transparency} {} string str() const override { ostringstream oss; oss << shape.str() << "has" << static_cast<float>()/ 255.f*100.f << "% transparency"; return oss.str(); } }
|
当我们需要一个有颜色和 透明度的形状时, 不需要再定义新的类, 而是这样实现:
1 2 3 4 5 6 7
| TransparentShape myCircle{ ColoredShape{ Circle{23}, "green" }, 64 };
cout << myCircle.str();
|
当然, 它有一个问题. 下面的代码是合法的. 虽然在逻辑意义上是不合理的:
ColoredShape{TransparentShape{ColoredShape{...}}}
9.3 Static Decorator
在前面的例子中, 我们给Circle
类增加了一个方法resize()
. 它并不是Shape
接口的一部分, 因此无法通过decorator来调用它. 因此, 下面的代码是无法通过编译的:
1 2 3
| Circle circle{3}; ColoredShape redCircle{circle, "red"}; redCircle.resize(2);
|
如果你并不需要一定要在运行时组合对象, 但是却需要访问被装饰的对象的所有字段和方法, 该怎么办?
实际上, 我们可以通过模板和继承实现–这里的继承并不是前面所说的会导致子类爆炸的继承. 我们会使用一种被称为Mixin继承的技术, 简单的说, 一个类派生于它的模板参数.
下面我们创建一个新的ColoredShape
类, 它派生于它的模板参数. 我们没有办法约束模板参数的类型, 因此我们使用static_assert
来检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename T> struct ColoredShape : T { static_assert(is_base_of<Shape, T>::value, "Template argument must be a Shape"!);
string color; string str() const override { ostringstream oss; oss << T::str() << "has the color " << color; return oss.str(); } }
|
同样我们可以实现TransparentShape
. 然后就可以组合两个类实现colored transparent shape:
1 2 3 4 5 6
| ColoredShape<TransparentShape<Square>> square{"blue"}; square.size = 2; square.transparency = 0.5; cout << square.str();
square.resize(3);
|
但是这么做还不够好, 我们希望能够在一行代码中实现对size
, color
, transparency
的设置.
我们回过头来重写它的构造函数, 这回构造函数包含两部分: 第一部分是这个类自己的参数, 第2部分是给基类的generic parameter pack. 按照这个方式来写TransparentShape
类:
1 2 3 4 5 6 7 8 9 10 11 12
| template <typename T> struct TransparentShape : T { static_assert(...); uint_8 transparency;
template <typename...Args> TransparentShape(const uint8_t transparency, Args ...args) : T(std::forward<Args>(args)...) , transparency{transparency} {} ... };
|
注意不要将构造函数设置为explicit
, 不然会和C++的copy-list-initialization冲突:
1 2
| ColoredShape2<TransparentShape2<Square>> sq = {"red", 51, 5}; cout << sq.str() << endl;
|
9.4 Functional Decorator
装饰器模式不仅可用于类, 也同样可以用于函数上.
考虑一个例子, 比如, 我们想在进入/退出函数时打印日志, 当然我们可以直接在代码中写:
1 2 3
| cout << "Enter function\n" ; ... cout << "Exit function\n";
|
这样当然可行, 但是我们希望能够做的更好–我们希望能够将这个功能逻辑单独保存起来, 从而可以在别处重用.
实现这个功能有很多方法. 下面是一种做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct Logger { std::function<void()> func; string name;
Logger(const std::function<void()>& func, const string& name) : func{func}, name{name} {} void operator()() const { cout << "Enter " << name << endl; func(); cout << "Exiting " << name << endl; } };
|
我们可以这样使用它:
1
| Logger([](){cout << "Hello" << endl; }, "HelloFunction")();
|
另一种变体是传入一个模板参数而不是std::function
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| template <typename Func> struct Logger2 { Func func; string name;
Logger(const Func& func, const string& name) : func{func}, name{name} {}
void operator()() const { cout << "Enter " << name << endl; func(); cout << "Exiting " << name << endl; } }
template <typename Func> auto make_logger2(Func func, const string& name) { return Logger<Func>{func, name}; }
|
就可以这样使用:
1 2
| auto call = make_logger([](){cout << "Hello!" << endl;}, "HelloFunction"); call();
|
关键点是什么? 在于我们可以创建一个装饰器(里面有一个函数)并在需要的时候调用它.
现在, 另一个问题: 如何在add()
被调用时以西面的格式打印日志, 并且要能得到返回值?
1 2 3 4 5
| double add(double a, double b) { cout << a << "+" << b << "=" << (a+b) << endl; return a+b; }
|
我们定义另一个logger:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| template <typename R, typename... Args> struct Logger3<R(Args...)> { Logger3(std::function<R(Args...)> func, const string& name) : func{func}, name{name} {}
R operator() (Args... args) { cout << "Enter " << name << endl; R result = func(args...); cout << "Exiting " << name << endl; return result; }
function<R(Args...)> func; string name; };
template <typename R, typename... Args> auto make_logger3(R(*func)(Args...), const string& name) { return Logger3<R(Args...)>( std::function<R(Args...)>(func), name ); }
|
我们就可以这样用:
1 2 3
| auto logged_add = make_logger3(add, "Add"); auto result = logged_add(2,3);
|