假设你正在使用某个由你的同事所开发的类, 你想扩展它的某个功能. 如何在不修改代码的前提下实现这一点? 一个方法是继承.

但是, 不是所有的情况下都可以使用继承. 比如, 你无法从std::vector来派生, 因为它没有虚拟析构函数; 你同样无法从int来派生. 更重要的是, 你需要的是某些增强, 而你希望将这些增强独立出来, 因为单一职责原则

9.1 场景

考虑一个称为”multiple enchancement”的例子. 你有一个称为Shape的类, 有两个派生类: ColoredShapeTransparentShape, 但是你还要考虑第三种情况: 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); // won't compiled!

如果你并不需要一定要在运行时组合对象, 但是却需要访问被装饰的对象的所有字段和方法, 该怎么办?

实际上, 我们可以通过模板和继承实现–这里的继承并不是前面所说的会导致子类爆炸的继承. 我们会使用一种被称为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}; // ()=call now
}

就可以这样使用:

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