闲谈组合模式——基于UI动画框架

15 篇文章 0 订阅
9 篇文章 0 订阅

  前几篇文章讨论了UI动画框架中应用的最大的设计模式:“解释器模式”,本文接着讨论框架中应用了两次的设计模式:“组合模式”。

1. 组合模式

  先来看下组合模式的定义:

将对象组合成树型结构以表示“部分 - 整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

  这个定义看起来舒服一点。再看一下类图:

组合模式类图

  组合模式的关键是抽象一个类,既可以表示叶子对象,又可以表示组合对象,所以叶子对象和组合对象都要从统一的接口类派生,这样客户代码就可以用同样的方法访问叶子对象和组合对象,而不用区分叶子对象和组合对象。Component接口提供典型的几种操作:

  • AddChild
  • RemoveChild
  • GetChild

  这几个操作对组合对象类是有意义的,因为组合对象持有叶子对象或组合对象(递归)的集合;但对于叶子对象类没什么意义,因此叶子对象中,这几个接口通常都是空操作,或者输出错误日志,或者抛出异常。

  通常,组合模式会组合出树型结构来,将多个组件对象自然地形成了对象树。因为组合对象还可以继续包含组合对象(数组),因此形成了递归关联,客户的代码在实现中不可避免的会出现递归调用的代码结构。因为要向客户代码提供统一的操作,因此抽象接口类中的接口是既支持叶子节点,又支持组合节点的接口的大杂烩。有一种安全的实现是将有关组合对象的接口放到Composite对象类中,那么客户代码就不太可能针对叶子节点调用这些接口,但是这样以来,客户代码就需要区分叶子对象和组合对象了。

  有时候,Component中需要提供一个接口:isLeaf或者isComposite,供客户代码判断是叶子对象还是组合对象。通常来说,组合模式假设客户代码不需要知道这个信息,但有的应用场景确实有这种需要,假如对象树需要以图形的方式显示出来,而显示层逻辑在其它语言中(比如Lua)实现,这些语言不支持面向对象编程模型,那就有很多地方需要知道具体的对象是叶子还是组合节点了,比如右键菜单:叶子节点的右键菜单和组合节点的右键菜单不一样(这个很正常),或者两者的图标不一样。

  组合模式中,一个节点自带到子节点的引用,在有些场合,可能有需要回溯到父节点,因此可以在实现中增加一个到父节点对象的引用(还是Component类型)。

  组合模式,相对来说是一个比较容易理解的设计模式,大家每天接触到的电脑上的“文件夹-文件”结构,几乎和组合模式的结构完美对应。在前面解释器的两篇文章中,大家已经看到了动画框架中有两个符合组合模式的结构,下面分别进行讨论。

2. 动画组合模式

  先看下动画类的结构图:

在这里插入图片描述
动画组由单个动画或动画组构成,对于客户代码来说,操纵动画组和操纵单个动画并没什么区别,都是在操纵一个Animation对象,而动画组的结构又是树形结构,因此组合模式是天然的选择。

  GroupAnimation从Animation接口类派生,因此动画组对客户代码来说,就像动画一样;同时GroupAnimation又包含有单个动画或者动画组,因为SingleAnimation也是从Animation类派生,所以,GroupAnimation只需持有到Animation接口类的(多个)引用;这种“既从Animation派生又持有到Animation句柄的引用”的递归嵌套结构,就自然地构成了树形结构。

  GroupAnimation要再实现AddAnimation接口,才能构建树形结构:

void GroupAnimation::AddAnimation(Animation* animation)
{
    if (animation) animations.push_back(animation);
}

  GroupAnimation是树形结构,因此如果需要操作树上的某一个对象,需要以一个索引序列在树上定位出目标对象(索引序列实际上是目标对象在树上的每一层枝杈的索引组合),比如要设置一个animation中某个strategy的参数:

bool GroupAnimation::SetStrategyParm(vector<int>& ids, const String& parm)
{
    if (ids.empty()) return false;
    int id = ids.back();
    ids.pop_back();
    if (id < 0 || id >= animations.size()) return false;
    return animations[id]->SetStrategyParm(ids, parm);
}

bool SingleAnimation::SetStrategyParm(vector<int>& ids, const String& parm)
{
    return strategy && strategy->SetStrategyParm(ids, parm);
}

bool Strategys::SetStrategyParm(vector<int>& ids, const String& parm)
{
    if (ids.empty()) return false;
    int id = ids.back();
    ids.pop_back();
    if (id < 0 || id >= strategys.size()) return false;
    return strategys[id]->SetStrategyParm(ids, parm);
}

bool FrameStrategy::SetPolicyParm(vector<int>& ids, const String& parm)
{
    if (!ids.empty()) return false;
	
    if (!parm.empty())
    {
        int value = atoi(parm.c_str());
        interval = max(10, value);
    }
    
    return true;
}

  GroupAnimation封装了组合节点,动画框架中有两种GroupAnimation:GroupAnimationSerial(串行动画组)和GroupAnimationParallel(并行动画组)。这两者都从GroupAnimation派生:

在这里插入图片描述

这两者最明显的不同就是启动方式:

// 串行动画组
bool GroupAnimationSerial::Run(void)
{
    for (auto animation : animations)
        if (animation->Run()) // 串行动画,只要有一个子动画还能启动成功,就返回
            return true; // 返回值表面动画启动成功

    return false; // 返回值表面动画未启动
}

// 并行动画组
bool GroupAnimationParallel::Run(void)
{
    for (auto animation : animations)
        animation->Run(); // 所有子动画并行执行

    return true;
}

3. 策略组合模式

  动画框架中,动画策略也是一个树形结构,策略组是由单个策略或策略组构成:

在这里插入图片描述

对于客户代码来说,它只持有一个IStrategy的句柄,并不关心是单个策略还是策略组,只要完成指定的动画即可;因此组合模式又是一个很自然的选择。

  Strategys也从IStrategy接口类继承,并持有到IStrategy句柄的引用(多个),同时实现了AddStrategy接口:

void Strategys::AddStrategy(IStrategy* strategy)
{
    if (strategy)
        strategys.push_back(strategy);
}

  框架中也有两种Strategys:StrategysSerial(串行策略组)和StrategysParallel(并行策略组),这两者都从IStrategy派生:

在这里插入图片描述

这两者最明显的不同就是执行方式。具体代码可以参见前文的Strategys章节

4. 结语

  组合模式是相对比较容易理解的模式,在需要用到该模式的地方,都是相当自然的需求,一般都具有以下两个特色:

  • 逻辑上的对象具有“部分 - 整体”的层次结构
  • 客户有需要统一操作组合结构中的所有对象

  因此,不用刻意设计,组合模式就是你的自然选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值