【问题】
在CAD或画图软件设计设计中,会存在大量的基本体[2],如line、 circle、arc、 polyline、 sphere、 box等。在组织它们之间的关系的时候,一般会有如下的继承体系:
图表 1 基本体类结构
即对每一种基本体,都有一个class与之对应。若有100种基本体,那就得有100个class从Entity继承而来。
但从这些class本身来看,问题并没有多么严重,毕竟不同的基本体总得有个class或object异或别的什么东西与之对应。一个完整的画图软件总得有序列化机制、总得有个undo/redo机制、总得有个界面显示这些纷纷乱乱的基本体的参数以供交互式操作吧。因此,每个类又得加入save/load操作,undo/redo支持,再加上套UI class[3]。记得N年前做的一个CAX项目中,但为基本体写UI class就花了2W行代码,现在想想有些汗颜。
图表 2 属性类与基本体类关系
图表 3 CAD示例
在写了N多相似的类后未免让人厌倦,让人有种重构的冲动。实现界面与数据分离可以使用脚本的办法,在此不作赘述。
【解决方案】
对于一个绘图程序,其基本体主要有两部分内容:参数 + 构建方法。各种基本体之间的差异也存在于此。数据部分可以用一个variant[4]容器统一来描述,通过一个参数ID提取不同的参数。差异最大在于创建实体的方法,即作用于参数的行为。再次抽象后,通用基本体类型层次关系如下:
图表 4 通用基本体类型类结构
各种基本体类型都用Primitive统一描述,不再从它继承子类,而是实现不同的创建基本体的BuildMethod。最后用一个Factory模式管理各种BuildMethod,根据与Primitive绑定的BuildMethod ID,创建几何形状。
由此所带来的好处是:
1. Save/Load只需要在Primitive实现
2. Undo/Redo只需要在Primitive实现
3. 可以从Primitive的参数类型描述中提出GUI表现形式[5]
4. 可扩展性得到增强,新增的类型无需考虑以上三条。
由此所带来最大的坏处就是性能上有所损失。从variant得到具体的数据类型毕竟没有直接用原始的数据类型来的高效些。从使用的效果来看,并不会产生用户交互上的延迟。
【总结】
前后两种方法的主要差别在于是使用继承还是聚合,以及是如何使用继承的。二者本身没有优劣之分,但在具体的情景下就需要权衡利弊。
--- 力为
[1] 《设计模式》
[2] 指基本几何类型。不知道这世界上有多少基本体,在不同的领域中可能基本体的定义也不尽相同。建筑中,可能门,窗都算作是基本体。
[3] boost::serialization 提供了一种非侵入式的序列化方法,值得一试。
Undo/redo的实现可以参考GoF的Memento Pattern.
[4] variant的实现有多种方法,boost有两种,参考boost::any 与 boost::variant 的区别。
[5] 提取形式亦可参考如何从脚本提取UI的实现方法。