“拜托别让我失败!”——装饰器对模板方法的呼唤






4.68/5 (11投票s)
实现模板设计模式的类是“装饰器感知”的吗?
引言
本文是笔者在实现一个小框架时,一次偶然的发现。该框架包含一堆类,这些类实现了一些广泛使用的设计模式。其中一个类实现了模板方法模式。当时的情况是,需要在不修改该类的情况下,使其可扩展。装饰器模式似乎是扩展(装饰)该类的合适选择。
除了在代码中提供一定的复杂性和使用上的乐趣外,设计模式在混合使用时遇到的问题直到被问及才会显现。本文将解释在混合使用装饰器模式和模板方法模式时遇到的问题。文章还将讨论绕过混合使用上述设计模式可能遇到的问题的解决方案。想知道为什么这些模式不太兼容吗?请继续阅读。
假设
本文假设读者已掌握以下知识
- 面向对象编程
- 设计模式,特别是装饰器模式和模板方法模式
问题
考虑一个如下所示的 Shape
类,它是所有形状(如 Circle
、Rectangle
、Square
等)的基类。
class Shape
{
private: std::string _ type;
public: Shape(const std::string& type) : _ type (type)
{
}
public: std::string Type() const
{
return _type;
}
protected: virtual void CreateDC() = 0;
protected: virtual void InitDC() = 0;
protected: virtual void Paint() = 0;
protected: virtual void ReleaseDC() = 0;
public: void Draw()
{
cout << "Drawing " << Type();
CreateDC();
InitDC();
Paint();
ReleaseDC();
}
};
上面列出的 Shape
类通过 Draw
方法实现了模板方法设计模式。像 Circle
或 Rectangle
这样的自定义形状,为了被绘制,必须实现纯虚函数 – CreateDC
、InitDC
、Paint
、ReleaseDC
。
例如,一个 Circle
形状可以假设性地实现如下:
class Circle : public Shape
{
public: Circle() : Shape("Circle")
{
}
protected: void CreateDC()
{
cout << std::endl << "Circle::CreateDC";
}
protected: void InitDC()
{
cout << std::endl << "Circle::InitDC";
}
protected: void ReleaseDC()
{
cout << std::endl << "Circle::ReleaseDC";
}
protected: void Paint()
{
cout << std::endl << "Circle::Paint";
}
};
上面类的典型用法如下:
void main()
{
//
// Previous Code...
//
Shape* s = new Circle();
s->Draw();
//
// Later Code...
//
}
上述程序的输出应该显而易见。
在任何应用程序开发的叙述中,Circle
都必须在不修改现有类的情况下进行扩展。例如,Circle
可以被填充;其边框颜色和粗细可以被改变,等等。假设我们想填充 Circle
。我们有几个选择:
- 从
Circle
派生一个类,例如FilledCircle
,并覆盖Paint
方法 – 这种技术不可扩展,也不能用于填充其他类型的形状。此外,这种方法会导致类数量的 组合爆炸,具体取决于所需的扩展。 - 使用一个装饰器,例如
ShapeFiller
– 这种技术极具可扩展性,因为我们可以将其应用于任何Shape
类。
根据讨论的主题,我们选择方案 #2。
以下是 ShapeFiller
(装饰器)类的假设性实现:
class ShapeFiller : public Shape
{
private: Shape* _shape;
public: ShapeFiller(Shape* shapeTobeFilled) : Shape(shapeTobeFilled->Name()),
_shape(shapeTobeFilled)
{
}
protected: void CreateDC()
{
// CDC1: Call the underlying shape's CreateDC
_shape->CreateDC();
cout << std::endl << "ShapeFiller::CreateDC";
}
protected: void InitDC()
{
// IDC1: Call the underlying shape's InitDC
_shape->InitDC();
cout << std::endl << "ShapeFiller::InitDC";
}
protected: void ReleaseDC()
{
// RDC1: Call the underlying shape's ReleaseDC
_shape->ReleaseDC();
cout << std::endl << "ShapeFiller::ReleaseDC";
}
protected: void Paint()
{
// Paint1: First call the underlying shape's Paint
_shape->Paint();
// Paint2: Custom logic to fill the shape (forget the color).
// Do you see it filling?
cout << std::endl << "ShapeFiller::Paint";
}
};
以上实现中有几点值得注意:
- 上述类将无法编译,并会报以下错误:
error C2248: 'Shape::CreateDC' : cannot access protected member declared in class 'Shape' error C2248: 'Shape::InitDC' : cannot access protected member declared in class 'Shape' error C2248: 'Shape::ReleaseDC' : cannot access protected member declared in class 'Shape' error C2248: 'Shape::Paint' : cannot access protected member declared in class 'Shape'
- 尽管
ShapeFiller
类的CreateDC
、InitDC
和ReleaseDC
方法不需要自定义逻辑,但它们必须实现,仅仅作为占位符,将调用转发给底层形状(这些方法是纯虚函数,无需赘述)。最快捷的解决方案似乎是在Shape
类中为上述方法提供默认(空)实现。这样做的话,在使用ShapeFiller
类时将不会创建(初始化和释放)DC。换句话说,应用程序在尝试Paint()
时会行为异常(可能崩溃)。 - 另一种(丑陋的)解决方案是将
protected
方法 –CreateDC, InitDC, ReleaseDC
– 设为public
。这是一个糟糕的设计选择。在面向对象的世界里,这是一种罪过。
这就是问题所在 – **尝试使用装饰器设计模式来扩展实现模板方法设计模式的类,会导致一种近乎困境的局面。**
装饰器模式的意图 [GoF]
- 动态地附加额外的职责到对象上。
- 装饰器为扩展功能提供了一种比子类化更灵活的替代方案。
模板方法模式的意图 [GoF]
- 在操作中定义一个算法的骨架,将一些步骤延迟到子类中。
- 模板方法允许子类重新定义算法的某些步骤,而不允许它们改变算法的结构。
如意图所述,装饰器模式避免了子类化,而模板方法模式依赖于子类化。其次,模板方法不必公开算法中涉及的所有步骤。另一方面,装饰器模式依赖于它打算装饰的类的 public
接口。显然,在我们这个例子中,采用装饰器模式会破坏模板方法模式的目的。
结论 – 有解决方案吗?
有,也有没有。
是
方法 1:使使用模板方法设计模式的类具备装饰器模式意识。
考虑新的 Shape
类:
class Shape
{
friend class ShapeDecoratorBase; // <== Added friend declaration
private: std::string _type;
public: Shape(const std::string& type) : _type (type)
{
}
public: std::string Type() const
{
return _type;
}
protected: virtual void CreateDC() = 0;
protected: virtual void InitDC() = 0;
protected: virtual void Paint() = 0;
protected: virtual void ReleaseDC() = 0;
public: void Draw()
{
cout << "Drawing " << Type();
CreateDC();
InitDC();
Paint();
ReleaseDC();
}
};
与为每个单独的装饰器编写冗余的代码来转发对底层 shape
对象的调用相比,引入了一个通用的 ShapeDecoratorBase
基类。换句话说,ShapeDecoratorBase
真正实现了装饰器模式。此类被设为 Shape
类的 **友元**(是的,友元。有些读者可能会认为使用友元是糟糕的设计。但并非如此,请参阅链接“何时使用友元”)。各个装饰器现在可以从 ShapeDecoratorBase
派生,并实现 Paint
方法来装饰底层形状。
ShapeDecoratorBase
类的定义如下:
class ShapeDecoratorBase : public Shape
{
protected: Shape* _shape;
public: ShapeDecoratorBase (Shape* shapeTobeFilled) : Shape(shapeTobeFilled->Name()),
_shape(shapeTobeFilled)
{
cout << std::endl << "ShapeDecoratorBase Ctor";
}
public: ~ShapeDecoratorBase()
{
cout << std::endl << "ShapeDecoratorBase Dtor";
}
protected: void CreateDC()
{
cout << std::endl << "ShapeDecoratorBase::CreateDC";
// CDC1: Call the underlying shape's CreateDC
_shape->CreateDC();
}
protected: void InitDC()
{
cout << std::endl << "ShapeDecoratorBase::InitDC";
// IDC1: Call the underlying shape's InitDC
_shape->InitDC();
}
protected: void ReleaseDC()
{
cout << std::endl << "ShapeDecoratorBase::ReleaseDC";
// RDC1: Call the underlying shape's ReleaseDC
_shape->ReleaseDC();
}
protected: void Paint()
{
cout << std::endl << "ShapeDecoratorBase::Paint";
// Paint1: First call the underlying shape's Paint
_shape->Paint();
}
};
这是 ShapeFiller
装饰器类的修订实现:
class ShapeFiller : public ShapeDecoratorBase
{
public: ShapeFiller(Shape* shapeTobeFilled) : ShapeDecoratorBase(shapeTobeFilled)
{
cout << std::endl << "ShapeFiller Ctor";
}
public: ~ShapeFiller()
{
cout << std::endl << "ShapeFiller Dtor";
}
protected: void Paint()
{
ShapeDecoratorBase::Paint();
// Paint2: Custom logic to fill the shape (forget the color).
// Do you see it filling?
cout << std::endl << "ShapeFiller::Paint";
}
};
现在 ShapeFiller
类如预期般工作。我们现在可以实现更多类似的类,如 ShapeBorderThickener
等。
方法 2:使用成员函数指针来实现模板方法设计模式。由于这种技术比方法 1 来说不够优雅,这里不再深入讨论。换句话说,它会伤害面向对象的程序员的眼睛。不过,源代码已附上供参考。
缺点
这两种方法的主要缺点是它们都需要访问基类的源代码(在本例中是 Shape
类)。在所有情况下,情况可能并非如此。
如果框架的实现者没有预见到这个问题,那么就没有优雅的解决方案。
无需多言,ShapeDecoratorBase
类必须由框架设计者提供,而不是框架的用户。
必须理解的是,不仅仅是模板方法类需要具备装饰器意识,程序员/框架设计者也必须意识到这种情况。由框架设计者决定一个类是否需要具备可扩展性。
否
如果无法访问基类的源代码,则无法解决此问题。如果模板方法类未实现为具备装饰器意识,则无法使用装饰器模式。
另一种选择是继承,但继承本身也存在问题。
最后的定论
我们希望本文揭示了在使用装饰器设计模式与模板方法设计模式时的一个微妙而有趣的问题。
"在实现框架时,请考虑您的模板方法设计模式类是否需要具备装饰器意识。"
设计愉快!
历史
- 2009 年 9 月 4 日:初稿
- 2009 年 9 月 8 日:根据 UGoetzke 的评论纠正了拼写错误
- 2009 年 9 月 9 日:添加了 C# 代码示例
- 2009 年 9 月 13 日:重写了引言