天天看点

闲谈装饰模式——基于UI动画框架1. 装饰模式2. 框架中的应用3. 结语

  本文讨论UI动画框架中应用过的一个设计模式:“装饰模式”。为什么是“应用过的”,而不是“正在用的”?原因是装饰模式在UI动画框架中的应用是一个失败的尝试,有那么点牵强,或者说“为了模式而模式”。装饰模式在框架中并没有存在多久,就被优化掉,用组合模式替代了。

1. 装饰模式

  装饰模式的定义:

动态的给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成派生类更灵活。

  类图:

闲谈装饰模式——基于UI动画框架1. 装饰模式2. 框架中的应用3. 结语

  Component:定义了业务类提供的功能接口;

  ConcreteComponent:实现具体的功能,这个具体类就是被装饰的原始对象;

  Decorator:所有装饰类的基类,其接口需要和Component的接口一致(从Component派生来保证这一点),并持有一个到Component对象的句柄,这个对象就是被装饰的对象;

  ConcreteDecoratorX:具体的装饰对象,实现具体要向被装饰对象添加/删除的功能(在被装饰对象之前/之后添加/删除功能)。

  除了原始的ConcreteComponent对象可以被装饰,其它被装饰过的对象也可以再被装饰,因为Decorator持有的是Component对象句柄,而Decorator对象同时也是Component对象,因此一个展现给客户的Component对象通常像一个洋葱一样,层层嵌套装饰:

闲谈装饰模式——基于UI动画框架1. 装饰模式2. 框架中的应用3. 结语

  装饰模式从一个对象外部给对象增加功能,就是通过“组合”而不是通过“继承”来扩展功能。不同的装饰器之间尽量独立(没有耦合关系),这样就可以随意改变组合顺序,复用性高,更为灵活。

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; // 替换
}
           

经过解析,上面的动画配置生成这样一个层层嵌套的对象:

闲谈装饰模式——基于UI动画框架1. 装饰模式2. 框架中的应用3. 结语

在执行时,在被装饰的对象执行的基础上,附加自己的功能:

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动画框架在初期采用装饰模式,属于设计的缺陷,很快就回归到“组合模式”。本系列单列一篇讨论这个模式,主要目的是加深对装饰模式的理解,从失败的设计中吸取可借鉴的经验。装饰模式的用场主要有:

  • 如果需要在不影响其它对象的情况下,以动态透明的方式给对象添加职责,可以使用装饰模式,这几乎是装饰模式的主要应用场景;
  • 有明显的需要在使用一个对象之前&之后都要附加一些功能的情况,比如前置条件/后置条件的检查。

如果没有以上特征,则不适宜用装饰模式。以动画框架中的策略体系类比,本来是一棵树,却偏要将所有树叶嵌进树枝,再将树枝折进树干,最后把树干塞到树根里面,生生揉成一个“洋葱”,用这样的设计框架,有如自缚手脚,寸步难行!

继续阅读