前几篇文章讨论了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. 结语
组合模式是相对比较容易理解的模式,在需要用到该模式的地方,都是相当自然的需求,一般都具有以下两个特色:
- 逻辑上的对象具有“部分 - 整体”的层次结构
- 客户有需要统一操作组合结构中的所有对象
因此,不用刻意设计,组合模式就是你的自然选择。