考虑用网页的形式表现一个如Adapter Report的测试报告:一个报表由2部分组成,一是封面,一是对各个测试项目及其结果的描述。每个测试项目分为项目名称,测试数量,测试条件,测试结果,结果描述等几项。其中测试数量和测试条件可能不会出现,可能出现,结果描述有时需要以表格或列表的形式列出详细的数据,如果是以表格的形式,则有可能有是多级表头,每个表格的列数并不固定。总的来说,这个报表是树型结构,如下图所示:
这时的简单类图如下所示:
封面跟报表是1-1的关系,其属性比如修订版次,PN等完全可以作为Report类的属性,所以可以合并成一个类。报表的所有组成部分都必须被转换成HTML,所以应该有一个叫做ToHTML的方法。而且,除了单个的测试条件和单个的单元格以外,其余的类都由若干个子组件组成,这些组件在转换成HTML的时候,除了把自身转换成HTML外,还需要调用子组件的ToHtml方法。把这些由子组件组成的类抽象一个新类,命名为ReportComposite。则Report、TableRow、TableDetails、ListDetails是他的子类。
我们再把他们抽象一下,把所有类增加子组件的方法叫做AddChild,所有移除子组件的方法叫做RemoveChild,所有表示子组件数量的属性叫做ChildCount,我们把这些公共的方法和属性抽象出来成为一个新类——ReportComponent。为了方便使用,我们还给ReportComponent加上了对迭代器的支持。TableCell、ListItem、ReportComposite是它的子类。
ReportComponent的代码如下:
public abstract class ReportComponent : IEnumerable<ReportComponent>
{
protected readonly List<ReportComponent> Children = new List<ReportComponent>();
public virtual int ChildCount()
{
return Children.Count;
}
public virtual ReportComponent GetChild(int Index)
{
if (Index >= 0 && Index < Children.Count)
return Children[Index];
return null;
}
public virtual void AddChild(ReportComponent AChild)
{
Children.Add(AChild);
}
public virtual void Remove(ReportComponent AChild)
{
if (Children.IndexOf(AChild) >= 0)
Children.Remove(AChild);
}
public abstract string ToHTML();
#region IEnumerable<ReportComponent> 成员
public IEnumerator<ReportComponent> GetEnumerator()
{
return Children.GetEnumerator();
}
#endregion
#region IEnumerable 成员
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return (Children as System.Collections.IEnumerable).GetEnumerator();
}
#endregion
}
ReportComposite的代码则是:
public abstract class ReportComposite:ReportComponent
{
public override string ToHTML()
{
StringBuilder sb = new StringBuilder();
foreach (ReportComposite item in Children)
sb.Append(item.ToHTML());
return sb.ToString();
}
}
他们的简单类图可以用如下形式所示(忽略了ReportComponent的基类):
现在和Composite模式的类图比较一下,发现只差一个Client了。现在,我们复习一下Composite设计模式的意图:“将对象组合成属性结构以表示‘部分——整体’的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。”其适用性是:
“你想表示对象的整体——部分层次结构。”
“你希望用户忽略组合对象与单个对象的不同,用户将统一的使用组合结构中的所有对象。”
我们在客户端的时候,只要一句Report.ToHtml()就可以把一份报表转换成网页,而不用考虑现在是在转换报表的封面还是转换一个个的测试项目,转换测试项目的时候,也不用考虑现在是转换的列表还是转换的是表格。总之,我们就只要ToHTML好了。
上述设计似乎符合要求了,但总觉得有些别扭。对TableCell、ListItem等叶节点而言,AddChild,RemoveChild,GetChild,ChildCount等操作都是没有意义的,而且,如果给Report的Children增加的是TableRow或这TableCell或者是TestCondition,系统也不能做出正确的反应,有时候,这是非常危险的。如果需要对这些行为做出正确的反应,就需要在子类中重写AddChild,RemoveChild等以对这些函数的操作对象进行限制(或者用Decorator模式来解决这个问题)。
另外,Report的TestItems和TestItem的详细描述SummaryDetails,现在都成了Children了,他们的一些重要信息,比如TestItem的ItemNo、ItemName也不能直接访问了,必须通过强制后转换才能获取。
对于TestItem类,更为要命的是,由于他的Children包含Conditions和SummayDetails组成,这两个属性都是由TableDetails和ListDetails混合组成,这样就导致TestItem不能从Children中正确的区分谁是Conditions的数据,谁是SummaryDetails的数据。显然,这不能解决我们的问题。看来对于TestItem,SummayDetails和Conditions,我们并不能“忽略组合对象与单个对象的不同”,也不能“统一的使用组合结构中的所有对象”,我们需要明确的区分谁是SummaryDetails,谁是Conditions,以便区别对待,把他们保存到TestItem相应的属性,或者取出相应的数据。所以不能用Composite模式来设计TestItem。既然这样,Report也不能用Composite来设计。虽然两者都有ToHTML的方法。只有对TestItem的SummayDetails/Conditions,不管他们是由TableDetails还是由ListDetails组成,抑或是由二者混合组成,我们都只把他们当作一个对象对待。所以只有他们才适用于Composite模式。
所以,我们必须把Report和TestItem从ReportComposite的子类中移除。这时的类图如下所示:
Report的代码如下:
public class Report
{
//
//省略的其他的属性
//
public string ToHTML()
{
StringBuilder sb = new StringBuilder();
sb.Append(SelfHtmlBegin());
foreach(TestItem item in TestItems)
sb.Append(item.ToHTML());
sb.Append(SelfHtmlEnd());
return sb.ToString();
}
List<TestItem> _TestItems = new List<TestItem>();
public List<TestItem> TestItems
{
get{ return _TestItems; }
}
}
TestItem的代码如下:
public class TestItem
{
//
//省略的其他的属性
//
List<ReportComponent> _Conditions = new List<ReportComponent>();
public List<ReportComponent> Conditions
{
get { return _Conditions; }
}
List<ReportComponent> _SummaryDetails = new List<ReportComponent>();
public List<ReportComponent> SummaryDetails
{
get { return _SummaryDetails; }
}
public string ToHTML()
{
StringBuilder sb = new StringBuilder();
foreach (ReportComponent item in Conditions)
sb.Append(item.ToHtml());
foreach (ReportComponent item in SummaryDetails)
sb.Append(item.ToHtml());
return sb.ToString();
}
}
实际上,Composite设计模式我们见得非常的多,看看表示层的那些控件,不管是WinForm还是WebForm,都有Composite模式的影子在内。
再看看我们的ReportComponent,对他的绝大部分的操作,都是调用其Children的操作,也就是List<T>的操作,这实际上就是Adapter设计模式的一个实例,Adapter模式的意图是:“将一类的接口转换成客户希望的另外一个接口,Adapter设计模式使得原本因为接口不兼容而不能一起工作的那些类可以一起工作。”它的适用性是:“
1、 你想使用一个存在的类,而它的接口不符合你的需求。
2、 你想创建一个可以复用的类,该类可以和其他不相关的类和那些不兼容的类(即那些接口可能不兼容的类)协同工作。
3、 (仅适用于对象Adapter)你想使用一些已经存在的类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。”
ReportComponent就是一个如下图所示的对象Adapter类。
Adapter设计模式我们平时也用得非常多,要重用现有的类库,就不可避免的要用到这个模式,只是许多时候我们没有意识到罢了。
详细的代码,请参见Composite.rar中的各个具体文件。
欢迎大家拍砖。
参考文件:《设计模式》中文版。