对于一个已经存在的类,如何在不修改原始代码的前提下扩展它的功能?
- 一种办法是继承,但继承有若干缺点,其中最关键的是,如果需要强化多个功能,并且由于单一职责原则,我们希望将这些强化的功能单独分开,这样就会产生非常多的派生类,导致代码管理非常困难。
装饰器模式,采用“组合”功能,可以达到强化对象的效果。具体分为两种不同的方法(以及其他几种模式,比如函数装饰器):
- 动态组合:允许在运行时组合某些东西,通常是通过按引用传递实现的。它的灵活性很强,因为组合可以在运行时响应用户的输入。
- 静态组合:对象及其强化功能是在编译时使用模板组合而成,意味着在编译时需要知道对象确切的强化功能,因为之后无法对其进行修改。
1. 动态装饰器
本节以为Shape对象扩展颜色和透明度为例:
struct Shape
{
virtual std::string str() const = 0;
};
struct Circle : Shape
{
float radius_;
explicit Circle(const float rad) : radius_{rad} {}
void resize(float factor) { radius_ *= factor;}
std::string str() const override
{
std::ostringstream oss;
oss << "A circle of radius " << radius_;
return oss.str();
}
};
struct Square : Shape
{
float side_length_;
explicit Square(const float len) : side_length_{len} {}
std::string str() const override
{
// std::string s;
// s = "A square of length " + std::to_string(side_length_);
// return s;
std::ostringstream oss;
oss << "A square of length " << side_length_;
return oss.str();
}
};
//动态装饰器
struct ColoredShape : Shape
{
Shape& shape_; //传入shape对象的引用
std::string color_;
ColoredShape(Shape& shape, const std::string& color)
: shape_{shape}, color_{color} {}
std::string str() const override
{
std::ostringstream oss;
oss << shape_.str() << " has the color " << color_ << "\n";
return oss.str();
}
void make_dark()
{
if(constexpr auto dark = "dark ";
/*!color_.starts_with(dark)*/true) //C++20新特性:starts_with()
{
color_.insert(0,dark);
}
}
};
struct TransparentShape : Shape
{
Shape& shape_;
uint8_t transparency_;
TransparentShape(Shape& shape, uint8_t trans_value)
: shape_{shape}, transparency_{trans_value} {}
std::string str() const override
{
std::ostringstream oss;
oss << shape_.str() << " has " << static_cast<float>(transparency_)/255.f*100.f << "% transparency\n";
return oss.str();
}
};
void test_colored_shape()
{
Circle cir(0.5f);
ColoredShape redCircle(cir, "red");
std::cout << redCircle.str();
redCircle.make_dark();
std::cout << redCircle.str();
}
void test_transparent_shape()
{
Square square{5};
TransparentShape demiSquare{square, 85};
std::cout << demiSquare.str();
}
void test_combination_shape()
{
Circle c{23};
ColoredShape cs{c, "green"};
TransparentShape myCircle{cs, 64};
std::cout << myCircle.str();
}
在以上的代码中,我们基于shape
扩展了带颜色功能的ColoredShape
,以及带透明度功能的TransparentShape
。并且动态装饰器方法的美妙之处在于,我们可以将ColoredShape
和TransparentShape
组合成一个同时具有颜色和透明度功能的形状。
2. 静态装饰器
在上一节的动态装饰器中,我们不能在ColoredShape
中使用Circle
的resize()
函数,因为它不是Shape
接口的一部分。能否构造一种装饰器,使它能够访问被装饰对象的所有属性成员和成员函数呢?答案是本节的的静态装饰器。
我们通过使用mixin继承的方式(类继承自它的模板参数)来实现该功能:
template <typename T>
struct ColoredShape : T
{
std::string color_;
std::string str() const override
{
std::ostringstream oss;
oss << T::str() << " has the color " << color_;
return oss.str();
}
};
template <typename T>
struct TransparentShape : T
{
uint8_t transparency_;
std::string str() const override
{
std::ostringstream oss;
oss << T::str() << " has the transparency " << static_cast<float>(transparency_)/255.f*100.f << "\n";
return oss.str();
}
};
很重要的一点是要确保模板类型参数继承自Shape,有两种办法:
- 使用
static_assert
和std::is_base_of_v
;
template typename<T> struct ColoredShape2 : T
{
static_assert(std::is_base_of_v<Shape, T>,
"Template argument must be a Shape");
//as before
};
- 使用concept;
==>c++20新语法,待研究;
接下来可以充分利用构造函数,以便能够通过一行代码来完整构造具有特定大小、颜色和透明度的Shape对象:使用“完美转发”的方法:
template<typename ...Args>
ColoredShape(std::string colr_str, Args... args)
: color_{colr_str}, T(std::forward<Args>(args)...) {}
template<typename ...Args>
TransparentShape(const uint8_t trans_value, Args... args)
: transparency_{trans_value}, T(std::forward<Args>(args)...) {}
在这种方法中,构造函数的参数数量必须准确,如果参数的数量或类型不准确,程序将无法编译。这对调用构造函数的方式施加了某些限制。
3. 函数装饰器
虽然装饰器模式通常应用于类,但同样也可以应用于函数。
例如,假设代码中有一个特定的函数给你带来了麻烦:你希望记录该函数被调用的所有时间,并在Excel中分析统计信息。
首先,这可以通过在调用函数前后放置一些代码来实现,例如:
std::cout << "Entering function XYZ\n";
//do the work
std::cout << "Exiting function XYZ\n";
从关注点分离的角度来看,这种方法并不好:我们更希望将日志功能存储在某个地方,以便复用并在必要时强化它。有不同的方法可以实现这一点:
一种方法是简单地将整个工作单元作为一个函数提供给某些日志组件:
struct Logger
{
std::function<void()> func_;
std::string name_;
Logger(const std::function<void()>& func, const std::string& name)
: func_{func}, name_{name} {}
void operator()()
{
std::cout << "Entering " << name_ << "\n";
func_();
std::cout << "Exiting " << name_ << "\n";
}
};
除此之外,我们可以将函数作为模板参数而不是std::function
传入:
template<typename Func>
class Logger2
{
Func func_;
std::string name_;
public:
Logger2(const Func& func, const std::string& name)
: func_{func}, name_{name} {}
void operator()() const
{
std::cout << "Entering " << name_ << "\n";
func_();
std::cout << "Exiting " << name_ << "\n";
}
};
现在,我们就有能力在需要装饰某个函数时创建一个装饰器(其中包含装饰的函数)并调用它。
如果需要在日志中记录函数的返回值,需要再次改进一下记录器:
template<typename R, typename... Args>
class Logger3
{
std::function<R(Args...)> func_;
std::string name_;
public:
Logger3(std::function<R(Args...)> f, const std::string& n) : func_{f}, name_{n} {}
R operator()(Args... args)
{
std::cout << "Entering " << name_ << std::endl;
R result = func_(args...);
std::cout << "Exiting " << name_ << std::endl;
return result;
}
};
4. 总结
装饰器在遵循开闭原则的同时为类提供了额外的功能。它的关键特点是可组合性,多个装饰器可以以任意顺序作用于对象:
- 动态装饰器:可以存储对被装饰对象的引用(如果需要,甚至可以存储整个值!)并提供动态(运行时)可组合性,但代价是无法访问底层对象自己的成员。
- 静态装饰器:使用mixin继承(从模板参数继承)在编译时组合装饰器,这虽然失去了运行时的灵活性(无法重新组合对象),但允许访问底层对象的成员。这些对象也可以通过构造函数转发进行完全初始化。
- 函数装饰器:可以封装代码块或特定函数,以允许组合行为。
值得一提的是,在不允许多重继承的语言中,装饰器还可以通过聚合多个对象来模拟多重继承,然后提供一个接口,该接口是聚合对象接口的集合的并集。