Skip to content

Latest commit

 

History

History
398 lines (292 loc) · 15.2 KB

File metadata and controls

398 lines (292 loc) · 15.2 KB

九、装饰器

假设您正在使用您同事编写的一个类,并且您想要扩展该类的功能。在不修改原始代码的情况下,你会怎么做呢?嗯,一种方法是继承:你创建一个派生类,添加你需要的功能,甚至可能是override什么的,然后你就可以开始了。

是的,除了这并不总是有效,原因有很多。例如,你通常不希望从std:: vector继承,因为它缺少虚拟析构函数,或者从int继承(那是不可能的)。但是,继承不起作用的最关键的原因是,在这种情况下,您需要几个增强,并且您希望保持这些增强是独立的,因为,您知道,单一责任原则。

装饰模式允许我们在不修改原始类型(开闭原则)或导致派生类型数量激增的情况下增强现有类型。

方案

让我解释一下多重增强的含义:假设你有一个名为Shape的类,你有两个名为ColoredShapeTranspar-entShape的继承者——你还需要考虑到有人想要一个ColoredTransparentShape的事实。所以我们生成了三个类来支持两个增强;如果我们有三个增强,我们将需要七个(7!)截然不同的阶层。让我们不要忘记,我们实际上想要不同的形状(SquareCircle等)。)—那些会从什么基类继承?有了三个增强和两个不同的形状,类的数量将跃升至 14。很明显,这是一种不可管理的情况——即使您正在使用代码生成工具!

让我们为此编写一些代码。假设我们定义了一个名为Shape的抽象类:

1   struct Shape
2   {
3     virtual string str() const = 0;
4   };

在前面的类中,str()是一个虚函数,我们将使用它来提供特定形状的文本表示。

我们现在可以用这个接口实现像CircleSquare这样的形状:

 1   struct Circle : Shape
 2   {
 3     float radius;
 4
 5     explicit Circle(const float radius)
 6       : radius{radius} {}
 7
 8     void resize(float factor) { radius *= factor; }
 9
10     string str() const override

11     {
12       ostringstream oss;
13       oss << "A circle of radius " << radius;
14       return oss.str();
15     }
16   }; // Square implementation omitted

我们已经知道,普通的继承本身并不能为我们提供增强形状的有效方法,所以我们必须求助于组合——这是装饰模式用来增强对象的机制。实际上有两种截然不同的方法,以及其他几种我们需要讨论的模式:

  • 动态组合允许你在运行时组合一些东西,通常是通过传递引用。它允许最大的灵活性,因为合成可以在运行时响应例如用户的输入而发生。
  • 静态组合意味着对象及其增强是在编译时通过使用模板来组合的。这意味着在编译时需要知道对象的确切增强集,因为以后不能修改它。

如果前面的内容听起来有点神秘,不要担心——我们将以动态和静态的方式实现装饰器,所以很快一切就都清楚了。

动态装饰器

假设我们想用一点颜色来增强形状。我们使用组合而不是继承来实现一个ColoredShape,它只是引用一个已经构造好的Shape并增强它:

 1   struct ColoredShape : Shape
 2   {
 3     Shape& shape;
 4     string color;
 5
 6     ColoredShape(Shape& shape, const string& color)
 7       : shape{shape}, color{color} {}
 8
 9     string str() const override

10     {
11       ostringstream oss;
12       oss << shape.str() << " has the color " << color;
13       return oss.str();
14     }
15   };

如你所见,ColoredShape本身就是一个Shape。您通常会这样使用它:

1   Circle circle{0.5f};
2   ColoredShape redCircle{circle, "red"};
3   cout << redCircle.str();
4   // prints "A circle of radius 0.5 has the color red"

如果我们现在想要另一个增强,增加形状的透明度,这也是微不足道的:

 1   struct TransparentShape : Shape
 2   {
 3     Shape& shape;
 4     uint8_t transparency;
 5
 6     TransparentShape(Shape& shape, const uint8_t transparency)
 7       : shape{shape}, transparency{transparency} {}
 8
 9     string str() const override

10     {
11       ostringstream oss;
12       oss << shape.str() << " has "
13         << static_cast<float>(transparency) / 255.f*100.f
14         << "% transparency";
15       return oss.str();
16     }
17   };

我们现在有一个增强,透明度值为 0..255 范围,并将其报告为百分比值。我们不能单独使用增强功能:

1   Square square{3};
2   TransparentShape demiSquare{square, 85};
3   cout << demiSquare.str();
4   // A square with side 3 has 33.333% transparency

但是最棒的是我们可以把ColoredShapeTransparentShape组合在一起,做出一个既有颜色又有透明度的形状:

1   TransparentShape myCircle{
2     ColoredShape{
3       Circle{23}, "green"
4     }, 64
5   };
6   cout << myCircle.str();
7   // A circle of radius 23 has the color green has 25.098% transparency

看到我在那里做了什么吗?我只是把整个事情都安排妥当了。现在,公平地说,你也可以做一件没有多大意义的事情,就是重复同一个装饰器一次。例如,有一个Colored-Shape{ColoredShape{...}}是没有意义的,但是它可以工作,给出了有些矛盾的结果。如果你决定用断言或一些 OOP 魔法来对抗它,你可以这么做,但是我想知道你将如何处理类似

1   ColoredShape{TransparentShape{ColoredShape{...}}}

这是更具挑战性的检测,即使这是可能的,我认为它根本不值得检查。我们需要假设程序员有一些理智。

静态装饰器

你注意到了吗,在设置场景时,我给了Circle一个名为resize()的函数,它不是Shape接口的一部分。正如您可能已经猜到的,因为它不是Shape的一部分,所以您真的不能从装饰器中调用它。我的意思是:

1   Circle circle{3};
2   ColoredShape redCircle{circle, "red"};
3   redCircle.resize(2); // won't compile!

假设你真的不在乎是否能在运行时组合对象,但是你真的在乎是否能访问一个修饰对象的所有字段和成员函数。有可能构造这样一个装饰器吗?

事实上,的确如此,它是通过模板和继承实现的——但不是那种导致状态空间爆炸的继承。相反,我们将应用一种叫做 Mixin 继承的东西,这是一种类从它自己的模板参数继承的方法。

所以这里有一个想法——我们将创建一个新的ColoredShape,它继承自一个模板参数。我们无法将模板参数约束为任何特定的类型,所以我们将使用一个static_assert来代替:

 1   template <typename T> struct ColoredShape : T
 2   {
 3     static_assert(is_base_of<Shape, T>::value,
 4       "Template argument must be a Shape");
 5
 6     string color;
 7
 8     string str() const override

 9     {
10       ostringstream oss;
11       oss << T::str() << " has the color " << color;
12       return oss.str();
13     }
14   }; // implementation of TransparentShape<T> omitted

有了ColoredShape<T>TransparentShape<T>的实现,我们现在可以将它们组合成一个彩色的透明形状:

1   ColoredShape<TransparentShape<Square>> square{"blue"};
2   square.size = 2;
3   square.transparency = 0.5;
4   cout << square.str();
5   // can call square's own members
6   square.resize(3);

这不是很棒吗?很好,但并不完美:我们似乎已经失去了对构造器的充分利用,所以即使我们能够初始化最外层的类,我们也无法在一行代码中完全构造出具有特定大小、颜色和透明度的形状。

来放糖衣(装饰品!)在我们的蛋糕上,给ColoredShapeTransparentShape转发构造器。这些构造器将接受两个参数:第一个是特定于当前模板类的参数,第二个是我们将转发给基类的通用参数包。我的意思是:

 1   template <typename T> struct TransparentShape : T
 2   {
 3     uint8_t transparency;
 4
 5     template<typename...Args>
 6     TransparentShape(const uint8_t transparency, Args ...args)
 7       : T(std::forward<Args>(args)...)
 8       , transparency{ transparency } {}
 9     ...
10   }; // same for ColoredShape

只是重申一下,前面的构造器可以接受任意数量的参数,其中第一个参数用于初始化透明度值,其余的只是转发给基类的构造器,不管它是什么。

构造器的数量自然必须正确,如果它们的数量或值类型不正确,程序将无法编译。如果您开始向类型中添加默认构造器,那么整个参数集的使用会变得更加灵活,但也会带来歧义和混乱。

哦,确保永远不要使用这些构造器explicit,否则在将装饰函数组合在一起时,会与 C++ 的复制列表初始化规则相冲突。现在,如何真正利用这些优点呢?

1   ColoredShape2<TransparentShape2<Square>> sq = { "red", 51, 5 };
2   cout << sq.str() << endl;
3   // A square with side 5 has 20% transparency has the color red

太美了!这正是我们想要的。这就完成了我们的静态装饰器的实现。同样,您可以增强它以避免重复类型,如ColoredShape<ColoredShape<…>>或循环类型,如ColoredShape<TransparentShape<ColoredShape<...>>>,但在静态环境中,这感觉像是浪费时间。不过,由于各种形式的模板魔术,这是完全可行的。

功能装饰

虽然装饰模式通常应用于类,但它同样可以应用于函数。例如,假设您的代码中有一个特殊的操作给您带来了麻烦:您希望在调用该操作时记录所有实例,并在 Excel 中分析统计数据。这当然可以通过在调用之前和之后添加一些代码来实现,也就是说:

1   cout << "Entering function\n";
2   // do the work
3   cout << "Exiting funcion\n";

这工作得很好,但是在关注点分离方面并不好:我们真的想将日志功能存储在某个地方,以便我们可以重用它,并在必要时增强它。

如何做到这一点有不同的方法。一种方法是简单地将整个工作单元作为 lambda 提供给某个日志组件,如下所示:

 1   struct Logger
 2   {
 3     function<void()> func;
 4     string name;
 5
 6     Logger(const function<void()>& func, const string& name)
 7       : func{func},
 8         name{name}
 9     {
10     }
11
12     void operator()() const

13     {
14       cout << "Entering " << name << endl;
15       func();
16       cout << "Exiting " << name << endl;
17     }
18   };

使用这种方法,您可以编写以下内容:

1   Logger([]() {cout << "Hello" << endl; }, "HelloFunction")();
2   // output:
3   // Entering HelloFunction
4   // Hello
5   // Exiting HelloFunction

总是有一个选项,不是作为一个std::function而是作为一个模板参数传入函数。这与前面的结果略有不同:

 1   template <typename Func>
 2   struct Logger2
 3   {
 4     Func func;
 5     string name;
 6
 7     Logger2(const Func& func, const string& name)
 8       : func{func}, name{name} {}
 9
10     void operator()() const

11     {
12       cout << "Entering " << name << endl;
13       func();
14       cout << "Exiting " << name << endl;
15     }
16   };

前面实现的用法完全相同。我们可以创建一个实用函数来实际创建这样一个记录器:

1   template <typename Func> auto make_logger2(Func func,
2     const string& name)
3   {
4     return Logger2<Func>{ func, name }; // () = call now
5   }

然后像这样使用它:

1   auto call = make_logger2([]() {cout << "Hello!" << endl; }, "HelloFunction");
2   call();

“有什么意义?”你可能会问。嗯……我们现在有能力创建一个装饰器(里面有装饰函数),并在我们选择的时间调用它。

现在,给你一个挑战:如果你想记录函数add()的调用,定义如下…

1   double add(double a, double b)
2   {
3     cout << a << "+" << b << "=" << (a + b) << endl;
4     return a + b;
5   }

但是你也想得到返回值?是的,从记录器返回一个返回值。没那么容易!但肯定不是不可能。让我们制作我们的记录器的另一个化身:

 1   template <typename R, typename... Args>
 2   struct Logger3<R(Args...)>
 3   {
 4     Logger3(function<R(Args...)> func, const string& name)
 5       : func{func},
 6         name{name}
 7     {
 8     }
 9
10     R operator() (Args ...args)
11     {
12       cout << "Entering " << name << endl;
13       R result = func(args...);
14       cout << "Exiting " << name << endl;
15       return result;
16     }
17
18     function<R(Args ...)> func;
19     string name;
20   };

在前面的例子中,模板参数R指的是返回值的类型,而Args,你肯定已经猜到了。装饰器保留该函数,并在必要时调用它,唯一的区别是operator()返回一个R,因此您不会丢失返回值。

我们可以构造另一个效用make_函数:

1   template <typename R, typename... Args>
2   auto make_logger3(R (*func)(Args...), const string& name)
3   {
4     return Logger3<R(Args...)>(
5       std::function<R(Args...)>(func),
6       name);
7   }

注意,我没有使用std::function,而是将第一个参数定义为一个普通的函数指针。我们现在可以使用这个函数实例化记录的调用并使用它:

1   auto logged_add = make_logger3(add, "Add");
2   auto result = logged_add(2, 3);

当然,make_logger3可以用依赖注入来代替。这种方法的好处是能够

  • 通过提供一个空对象 1 而不是一个实际的日志记录器来动态地打开和关闭日志记录
  • 禁用正在记录的代码的实际调用(同样,通过替换不同的记录器)

总而言之,这是开发人员工具箱上的另一个有用的工具。我将这种方法编织到依赖注入中作为读者的练习。

摘要

装饰器在遵循 OCP 的同时给了类额外的功能。它的关键方面是可组合性:几个装饰器可以以任何顺序应用于一个对象。我们已经了解了以下类型的装饰器:

  • 动态装饰器可以存储引用(如果你愿意,甚至可以存储整个值!)并提供动态(运行时)可组合性,代价是不能访问底层对象自己的成员。
  • 静态装饰器使用 mixin 继承(从模板参数继承)在编译时组成装饰器。这失去了任何类型的运行时灵活性(不能重新组合对象),但允许您访问底层对象的成员。这些对象也可以通过构造器转发完全初始化。
  • 函数装饰器可以包装代码块或者特定的函数,以允许行为的组合。

值得一提的是,在不允许多重继承的语言中,decorators 也用于模拟多重继承,它聚合多个对象,然后提供一个接口,该接口是聚合对象接口的集合并集。

Footnotes 1

Null 对象在本书第章第 19 章中有描述。本质上,空对象是符合某个接口的对象,但是有空方法,也就是说,完全不做任何事情的方法。这解决了当你必须提供一个对象到一个 API 中,但是你不希望这个对象实际上做任何事情时的问题。