本文讨论UI动画框架中应用过的一个设计模式:“装饰模式”。为什么是“应用过的”,而不是“正在用的”?原因是装饰模式在UI动画框架中的应用是一个失败的尝试,有那么点牵强,或者说“为了模式而模式”。装饰模式在框架中并没有存在多久,就被优化掉,用组合模式替代了。
1. 装饰模式
装饰模式的定义:
动态的给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成派生类更灵活。
类图:
Component:定义了业务类提供的功能接口;
ConcreteComponent:实现具体的功能,这个具体类就是被装饰的原始对象;
Decorator:所有装饰类的基类,其接口需要和Component的接口一致(从Component派生来保证这一点),并持有一个到Component对象的句柄,这个对象就是被装饰的对象;
ConcreteDecoratorX:具体的装饰对象,实现具体要向被装饰对象添加/删除的功能(在被装饰对象之前/之后添加/删除功能)。
除了原始的ConcreteComponent对象可以被装饰,其它被装饰过的对象也可以再被装饰,因为Decorator持有的是Component对象句柄,而Decorator对象同时也是Component对象,因此一个展现给客户的Component对象通常像一个洋葱一样,层层嵌套装饰:
装饰模式从一个对象外部给对象增加功能,就是通过“组合”而不是通过“继承”来扩展功能。不同的装饰器之间尽量独立(没有耦合关系),这样就可以随意改变组合顺序,复用性高,更为灵活。
2. 框架中的应用
框架中的策略类体系的初版是使用装饰模式来实现策略组的。首先定义了一个装饰策略类:
class DecoratorStrategy : public IStrategy
{
public:
explicit DecoratorStrategy(IStrategy* ps) : strategy(ps) {}
virtual IStrategy* Clone() const;
private:
IStrategy* strategy;
};
该类从IStrategy类派生,并持有一个IStrategy句柄。从策略集合中挑一些基本的策略(即不装饰任何其它的策略)直接从IStrategy类派生,比如FrameStrategy就是一个基本的策略,这些类就是ConcreteComponent:
class FrameStrategy : public IStrategy
{
private:
mutable int interval; // 两次播放之间的间隔毫秒数
long lastTick; // 逝去的时间
};
其它的策略类都从DecoratorStrategy派生:
// 淡入淡出策略
class FadeStrategy : public DecoratorStrategy
{
public:
explicit FadeStrategy(IStrategy* ps) : DecoratorStrategy(ps) {}
private:
const float iniAlpha; // alpha初始值
float delta; // 帧与帧之间的alpha变化的百分比
const float minAlpha; // alpha的下限
const float maxAlpha; // alpha的上限
};
// 缩放策略
class ScaleStrategy : public DecoratorStrategy
{
public:
explicit ScaleStrategy(IStrategy* ps, float dx = 0, float dy = 0, int s = 0)
: DecoratorStrategy(ps), fdx(dx), fdy(dy), steps(s)
{
}
private:
float fdx; // x轴缩放步长(比例)
float fdy; // y轴缩放步长(比例)
int steps; // 步数
};
假如,有这样的配置:
<strategys id = "123" desc = "窗口慢慢向上飘起,慢慢缩小,并慢慢消失" >
<strategy id = "frame" param = "30" /> <!-- 30ms一帧 -->
<strategy id = "move" param = "vertical;10;-20" /> <!-- 从起始位置向上移动10次,每次20像素 -->
<strategy id = "scale" param = "1;0.8;0.8;6" /> <!-- 尺寸缩小10次,每次长宽均缩小0.8 -->
<strategy id = "fade" param = "1;0.8" /> <!-- alpha从1.0起每次淡化0.8 -->
</strategys>
解析过程是这样的:
void AnimationParser::BeginStrategy(const AttriMap& attributes)
{
IStrategy* & ps = strategys.back(); // 取出前一个策略对象
IStrategy* p = nullptr;
if (attributes["id"] == "frame"):
p = new FrameStrategy(ps, attributes["param"]); // 装饰前面的策略对象
else if (attributes["id"] == "move"):
p = new MoveStrategy(ps, attributes["param"]); // 装饰前面的策略对象
else if (attributes["id"] == "scale")
p = new ScaleStrategy(ps, attributes["param"]); // 装饰前面的策略对象
else if (attributes["id"] == "fade")
p = new FadeStrategy(ps, attributes["param"]); // 装饰前面的策略对象
...
else
throw ("invalid strategy type!") + attributes["id"]);
ps = p; // 替换
}
经过解析,上面的动画配置生成这样一个层层嵌套的对象:
在执行时,在被装饰的对象执行的基础上,附加自己的功能:
bool FadeStrategy::Run(Window& aniWnd)
{
DecoratorStrategy::Run(aniWnd); // 被装饰的对象执行它的功能
// 以下执行自己的功能
float alpha = aniWnd.getAlpha();
if ((delta < 1.0 && alpha >= minAlpha) || (delta > 1.0 && alpha < maxAlpha))
{
float xalpha = alpha * delta;
aniWnd.setAlpha(xalpha > 1.0f ? 1.0f : xalpha);
}
return true;
}
执行顺序是这样的:执行FrameStrategy → 执行MoveStrategy → 执行ScaleStrategy → 执行FadeStrategy。
用装饰模式实现的策略体系,看上去也挺不错的。但随后就遇到了诸多不便。
✘ 结构僵化
根据动画配置规则,策略(们)是一个天然的树形结构,“组合模式”的结构对树形结构的模拟比较自然,而“装饰模式”的结构就显得有点僵硬。组合模式中在组合节点上通过索引在子对象列表(数组)中直接定位到子对象,而在装饰模式中,则不存在这样的列表,需要像剥洋葱似的,一层层向里层推进,并用一个计数器来标记层次,直到找到目标节点。在实际应用中,你会觉得不顺畅,有种别别扭扭的感觉。
✘ 效率低
从上面的代码实例中可以看出,一个策略组中最先执行的策略位于“洋葱”对象的最里层,需要一层层向里传递调用,另外,如果一个策略的控制还没有完成,退出时,得另外标记,以便下一帧能找到它继续执行。在实现时,就不只是别扭了,而是觉得处处受限,需要做不少无谓的簿记工作,以完成正确的控制流程。
3. 结语
UI动画框架在初期采用装饰模式,属于设计的缺陷,很快就回归到“组合模式”。本系列单列一篇讨论这个模式,主要目的是加深对装饰模式的理解,从失败的设计中吸取可借鉴的经验。装饰模式的用场主要有:
- 如果需要在不影响其它对象的情况下,以动态透明的方式给对象添加职责,可以使用装饰模式,这几乎是装饰模式的主要应用场景;
- 有明显的需要在使用一个对象之前&之后都要附加一些功能的情况,比如前置条件/后置条件的检查。
如果没有以上特征,则不适宜用装饰模式。以动画框架中的策略体系类比,本来是一棵树,却偏要将所有树叶嵌进树枝,再将树枝折进树干,最后把树干塞到树根里面,生生揉成一个“洋葱”,用这样的设计框架,有如自缚手脚,寸步难行!