设计模式
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第6章 创建者模式

6.1 经典回顾

6.2 异步调用的BuildUp()

6.3为Builder打个标签

6.4 具有装配/卸裁能力的Builder

6.5 看着图纸加工——登记配置文件

6.6 用迭代器控制流水线

6.7 小结

Separate the construction of a complex object from its representation so that the same construction process can create different representations..

Design Patterns : Elements of Reusable Object-Oriented Software

如果说上面工厂模式、抽象工厂模式,以及只维护单一实例的单件模式,它们都是在创建对象的话,那么Builder 模式就是其中的一个“精工细作”的工匠,因为它一般用于创建复杂对象,从独立创建每个部分到最后的组装,它承担每个步骤的工作。由于它把创建每个部分都独立为一个单一的过程,因此不仅可以完成较为“精细”的创建,还可以根据创建步骤编排,生成不同的目标实例。

就像我们之前在Singleton、Abstract Factory中对经典设计模式作扩展的时候,曾经介绍过的创建职责与指导创建职责分别被不同对象所承担一样,Builder 模式中的组装和执行次序编排也需要一个顾问来指导,也就是需要增加一个Director对象。

不仅如此,如图6-1所示,在实际项目中除了那种一味“添砖加瓦”构件的过程外,有时候还需要一个“拆除”的过程,也就是说,客户程序可能除了要调用一个个BuildUp()方法外,偶尔还需要调用TearDown(),目的是让对象的装配运行时动态化。在这方面,.NET平台有个非常不错的范本——ObjectBuilder,虽然很多非.NET社区将它轻描淡写为一个IOC容器,意图贬低ObjectBuilder的应用基础能力,但事实上ObjectBuilder已经成为官方版本公共库的Essential而非Optional组成了。

图6-1 示意的对象装配和拆解过程

相对于ObjectBuilder而言,本章介绍的Builder模式是其设计精髓的部分——实现对复杂对象反复锻造的过程。

6.1 经典回顾

经典设计模式中对创建者模式的描述“过于”精炼了,它表示创建者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而使一个创建过程生成具有不同的内部表象的产品对象。

很大程度上这个意图太过精炼以至于学习者很难把握它的要求,实质上创建者模式解决的问题是产品局部加工过程变化较大,但组装过程相对固定。例如:

● 一个最直接的例子就是“装”PC机,虽然声卡、显卡、内存、硬盘、机箱、键盘鼠标、显示器有很多的不同,无论是图形工作站还是一般娱乐用的笔记本,无论是品牌机还是自己“攒”的机器,每个部分的制造和生产过程都不同,但组装一台计算机的过程相对固定。

● 汽车的生产与此类似,虽然不同品牌、不同档次的汽车在轮胎、发动机、车身的制作工艺上有很大区别,但组装的过程相对而言非常稳定。

● ASP.NET应用也是如此,虽然搜索框、广告条、导航条、页面主体和页面页角部分的设计和实现有很大差别,但很多ASP.NET 应用的基本布局还是大致相同的,无论是静态网页还是动态网页,按照这个布局最后组装成一个浏览器以显示页面的过程相对也是固定的。

上面这些例子是创建者模式典型的应用场景,经典的创建者模式静态结构如图6-2所示。

图6-2 经典创建者模式的静态结构

其中包括三个角色。

● Builder(IBuilder):负责描述创建一个产品各个组成的抽象接口。

● Concrete Builder:实现Builder要求的内容,并且提供一个获得产品的方法。

● Director:虽然Builder定义了构造产品的每个步骤,但Director只是告诉如何借助Builder生成产品的过程,它对Builder的操作完全基于Builder的抽象方法。

区别于之前的各个设计模式,您可能发现这里的产品类型并没有用IProduct表示,主要原因是经过不同Concrete Builder加工后的产品差别相对较大,给它一个公共的基准抽象对象意义不大,而且您可以看到GetResult()方法仅仅位于实体类中。但在实际项目中,我们之所以使用创建者模式往往有很明确的意图,即主要用它来加工某一类产品,只不过系统通过不同的中间步骤“锻造”出具体组成上有所不同的同一类产品。相对而言,这种情况一般需要创建者定义的步骤非常宽泛,后一种情况由于加工的产品类型比较明确,所以创建者定义的每个BuildPart()相对也更专注于这类产品。

经典模式的一个示例代码如下。

C# 生产非同源产品的经典Builder模式实现

public class House
{
    public void AddWindowAndDoor() { }
    public void AddWallAndFloor() { }
    public void AddCeiling() { }
}
public class Car
{
    public void AddWheel() { }
    public void AddEngine() { }
    public void AddBody() { }
}
public interface IBuilder
{
    void BuildPart1();
    void BuildPart2();
    void BuildPart3();
}
public class CarBuilder : IBuilder
{
    private Car car;
    public void BuildPart1() { car.AddEngine(); }
    public void BuildPart2() { car.AddWheel(); }
    public void BuildPart3() { car.AddBody(); }
    public Car GetResult() { return car; }
}
public class HouseBuilder : IBuilder
{
    private House house;
    public void BuildPart1() { house.AddWallAndFloor(); }
    public void BuildPart2() { house.AddWindowAndDoor(); }
    public void BuildPart3() { house.AddCeiling(); }
    public House GetResult() { return house; }
}
public class Director
{
    public void Construct(IBuilder builder) //指导IBuilder的创建过程
    {
        builder.BuildPart1();
        builder.BuildPart2();
        builder.BuildPart3();
    }
}

C# 生产同源产品的经典Builder模式实现

public interface IProduct { string Name { get; set;} }
public class ConcreteProduct : IProduct
{
    protected string name;
    public string Name
    {
        get { return this.name; }
        set { this.name=value; }
    }
}
public interface IBuilder
{
    void BuildPart();
    /// 相当于GetResult()方法,由于构造的产品属于同一种类型,
    /// 因此这类Builder模式实现中,将该方法直接定义在抽象对象上。
    IProduct BuildUp();
}
public class ConcreteBuilderA : IBuilder
{
    private IProduct product=new ConcreteProduct();
    public void BuildPart() { product.Name="A"; }
    public IProduct BuildUp() { return product; }
}
public class Director
{
    public void Construct(IBuilder builder) { builder.BuildPart();}
    //指导IBuilder创建过程
}

通过上面对经典创建者模式的实现,我们不难发现使用它有很明显的优势,但也很容易产生不利的影响。

优势:

● 创建者模式将复杂对象的每个组成创建步骤暴露出来,借助Director(或客户程序自己)既可以选择其执行次序,也可以选择要执行哪些步骤。上述过程可以在应用中动态完成,相比较工厂方法和抽象工厂模式的一次性创建过程而言,创建者模式适合创建“更为复杂且每个组成变化较多”的类型。

● 向客户程序屏蔽了对象创建过程的多变性(相对工厂方法和抽象工厂模式而言,创建者模式在这方面更为明显)。

● 正如上面两个例子,构造过程的最终成果可以根据实际变化的情况,选择使用一个统一的接口,或者不同类的对象,给客户类型更大的灵活度。

劣势:相对而言,创建者模式会暴露出更多的执行步骤,需要Director(或客户程序)具有更多的领域知识,使用不慎很容易造成相对更为紧密的耦合。

6.2 异步调用的BuildUp()

如果之前看了本书的第1章,您可能对上面两个Director的实现表示不满,因为它们把要执行的步骤进行了硬编码,对于第一个例子,它只能按照BuildPart1、2、3的次序执行,如果要调整为1、3、2,那么就需要重新编码,但其实从委托的角度来说,这三个方法的抽象结构完全一样,委托定义为:

C#构造一个汽车类型时,每个BuildPart步骤的委托定义

public delegate void BuildStepHandler();

可以考虑使用一个通用的Director,它负责保存一个IList< BuildStepHandler >,Director在执行Constructor的时候依次遍历每个BuildStepHandler()即可,装配列表的任务则可以交给配置机制,或者是被外部注入。进一步考虑,既然 Director自己就是个BuildStepHandler列表,结构已经如此固定,一般情况下就无须专门定义个Director类型了,不妨直接写入 IBuilder的抽象基类里,这样我们获得修改后具有动态异步执行特征的设计,如图6-3所示。

图6-3 将创建步骤“委托化”之后的静态结构

相对经典创建者模式实现而言,它最大的优势在哪?灵活。将本来硬编码完成的步骤,通过对制造过程的抽象(委托),提炼出可动态装配的结构。我们来看一个示例,其代码如下:

C#

public interface IBuilder
{
    Car BuildUp();
}
public abstract class BuilderBase : IBuilder
{
    protected IList<BuildStepHandler> steps=new List<BuildStepHandler>();
    protected Car car=new Car();
    public virtual Car BuildUp()
    {
        foreach (BuildStepHandler step in steps)
          step();
        return car;
    }
}
public class ConcreteBuilder : BuilderBase
{
    /// 由于BuildStepHandler描述抽象的BuildPart()方法,因此实际项目中,
    /// ConcreteBuilder需要配置的委托可以统一通过访问配置文件的方式获取
    public ConcreteBuilder() : base()
    {
        steps.Add(car.AddEngine);
        steps.Add(car.AddWheel);
        steps.Add(car.AddBody);
    }
}

Unit Test

[TestMethod]
public void Test()
{
    IBuilder builder=new ConcreteBuilder();
    Car car=builder.BuildUp();
    Assert.IsNotNull(car);
}

现实情况中,一个产品在提供用户前是否真的都在Builder或Factory完成呢?很多时候不是。比如:生产新的MP3,在BuildUp()或Create()之前需要做市场调查,了解目标受众喜欢什么款式的;哪怕电子元件已经运到了生产部门,甚至到了生产线上也要对元件进行简单的筛选;流水线完成后,理论上产品已经生产出来了,但很少有MP3没有包装盒的,当然您可以把包装盒视为MP3这个产品的一个组成,那起码还有个抽检过程,也就是说,BuildStepHandler大致上被分解成预处理(Pre Process)、构造(Construct)和后续处理(Post Process)三种。从上面的描述您也可以看出,分类的主要现实原因是这三类处理是由不同角色发起的,或者说它们的层次不同:质量保证应该是组织级的,而实际加工步骤是某个产品(或某类产品)自己的。软件也一样,Pre Process可能是权限检查和构造参数检查之类的工作,Process则是构造产品类的过程……考虑到创建者模式处理的大部分是所谓的“复杂”对象,即它很可能要涉及几个子对象的组合,所以为BuildStepHandler 增加一个分类,即子对象的初始化并装配过程(Initialization),如表6-1所示。

表6-1 BuildStepHandler的分类

这样做的好处是可以在不同阶段抛出不同的事件,确保客户程序可以及时了解实际过程中某些步骤是否正常执行,整道工序执行到了什么位置。

6.3为Builder打个标签

6.3.1 完成工具类

上述过程中,我们通过将不同BuildPart()进行抽象,提供一个更具动态特征、更灵活的BuildUp()方法。但实施上为了真正让 ConcreteBuilder配置实际需要执行的每个委托,还需要借助外部机制(例如访问配置文件)来协助它完成。回想一下,现实中我们组装一台机器或组装一部汽车的时候,经常采取的办法是检查产品自己的说明书,然后开始执行BuildUp()的工作。对于待加工的对象是否也可以考虑采取类似的办法呢?可以,不过可能代价比较大,比如让每个产品都自己实现一个IList<BuildStepHandler>的属性,这样做太麻烦。.NET平台有个很好的机制——“贴标签”,也就是用Attribute来表示扩展目标产品类型的创建信息,这样,我们就可以完成一个非常通用的Builder类,由它“看着说明书”加工多种多样的产品。

工具类型设计

由于读取属性一般通过反射获得,考虑到后面的章节还会经常用到类似的功能,我们在第2章创建的工具箱里再增加一个新工具——AttribueHelper,其代码如下:

C#

/// 获取某个类型包括指定属性的集合
public static IList<T> GetCustomAttributes<T>(Type type) where T : Attribute
{
    if (type == null) throw new ArgumentNullException("type");
    T[] attributes=(T[])(type.GetCustomAttributes(typeof(T), false));
    return (attributes.Length == 0) ? null : new List<T>(attributes);
}
/// 获得某个类型包括指定属性的所有方法
public static IList<MethodInfo> GetMethodsWithCustomAttribute<T>(Type type)
where T : Attribute
{
    if (type == null) throw new ArgumentNullException("type");
    MethodInfo[] methods=type.GetMethods();
    if ((methods == null) || (methods.Length == 0)) return null;
    IList<MethodInfo> result=new List<MethodInfo>();
    foreach (MethodInfo method in methods)
        if(method.IsDefined(typeof(T), false))
          result.Add(method);
    return result.Count == 0 ? null : result;
}
/// 获取某个方法指定类型属性的集合
public static IList<T> GetMethodCustomAttributes<T>(MethodInfo method) where
T : Attribute
{
    if (method == null) throw new ArgumentNullException("method");
    T[] attributes=(T[])(method.GetCustomAttributes(typeof(T), false));
    return (attributes.Length == 0) ? null: new List<T>(attributes);
}
/// 获取某个方法指定类型的属性
public static T GetMethodCustomAttribute<T>(MethodInfo method) where T :
Attribute
{
    IList<T> attributes=GetMethodCustomAttributes<T>(method);
    return (attributes == null) ? null : attributes[0];
}

新的结构变化如图6-4所示。

图6-4 用定制Attribute指导后的静态结构

Sequence为某个方法在BuildUp过程中的次序,Times表示执行次数,Handler表示需要通过反射机制实际执行的目标方法。

定义用于指导BuildPart过程的属性

其示例代码如下:

C#

/// 指导每个具体类型BuildPart过程目标方法和执行情况的属性
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false)]
public sealed class BuildStepAttribute : Attribute, IComparable
{
    private int sequence;
    private int times;
    private MethodInfo handler;
    public BuildStepAttribute(int sequence, int times)
    {
        this.sequence=sequence;
        this.times=times;
    }
    public BuildStepAttribute(int sequence) : this(sequence, 1) { }
    ///该Attribute需要执行的目标方法
    public MethodInfo Handler
    {
        get { return handler; }
        set { this.handler=value; }
    }
    /// 标注为这个Attribute的方法,在执行过程中的次序
    public int Sequence { get { return this.sequence; } }
    /// 标注为这个Attribute的方法,在执行过程中执行的次数
    public int Times { get { return this.times; } }
    /// 确保每个BuildStepAttribute可以根据sequence比较执行次序
    public int CompareTo(object target)
    {
        if((target == null) || (target.GetType() !=
          typeof(BuildStepAttribute)))
          throw new ArgumentException("target");
        return this.sequence - ((BuildStepAttribute)target).sequence;
    }
}

借助这个属性,Builder可以获得执行某个BuildPart步骤的指导信息,包括该步骤的执行次序、需要执行的次数,以及通过反射获得的方法信息。

定义具有BuildPart自动发现机制的动态Builder

其示例代码如下:

C#

public interface IBuilder<T> where T : class, new()
{
    T BuildUp();
}
public class Builder<T> : IBuilder<T> where T : class, new()
{
    public virtual T BuildUp()
    {
        IList<BuildStepAttribute> attributes=DiscoveryBuildSteps();
        if (attributes == null) return new T();
        // 没有BuildPart步骤,退化为Factory模式
        T target=new T();
        foreach (BuildStepAttribute attribute in attributes)
          for (int i=0; i < attribute.Times; i++)
                attribute.Handler.Invoke(target, null);
        return target;
    }
    /// 借助反射获得类型T所须执行BuildPart()的自动发现机制
    protected virtual IList<BuildStepAttribute> DiscoveryBuildSteps()
    {
        IList<MethodInfo> methods=AttributeHelper.
          GetMethodsWithCustomAttribute<BuildStepAttribute>(typeof(T));
        if ((methods == null) || (methods.Count == 0)) return null;
        BuildStepAttribute[] attributes =
          new BuildStepAttribute[methods.Count];
        for (int i=0; i < methods.Count; i++)
        {
          BuildStepAttribute attribute=AttributeHelper.
              GetMethodCustomAttribute<BuildStepAttribute>(methods[i]);
          attribute.Handler=methods[i];
          attributes[i]=attribute;
        }
        Array.Sort<BuildStepAttribute>(attributes);
        return new List<BuildStepAttribute>(attributes);
    }
}

Unit Test

[TestClass]
public class TestBuilder
{
    public class Car
    {
        public IList<string> Log=new List<string>();
        [BuildStep(2)]
        public void AddWheel() { Log.Add("wheel"); }
        public void AddEngine() { Log.Add("engine"); }
        [BuildStep(1, 2)]
        public void AddBody() { Log.Add("body"); }
    }
    [TestMethod]
    public void Test()
    {
        Builder<Car> builder=new Builder<Car>();
        Car car=builder.BuildUp();
        Assert.IsNotNull(car);
        Assert.AreEqual<int>(2+1, car.Log.Count);
        //实际只执行了两个方法, 但进行了三次调用
        Assert.AreEqual<string>("body", car.Log[0]);    // 按照标注的次序执行
        Assert.AreEqual<string>("wheel", car.Log[2]);   // 按照标注的次序执行
    }
}

相信经过单元测试后,您将对重新定义的这个创建者刮目相看,因为无需Director的指导,它可以根据每个产品类型自己的定义,动态地找到它需要执行的每个BuildPart()步骤。它在实现上相对复杂了一些,不过放在项目中有如下优势:

● 不必反反复复地编写创建者甲、创建者乙,只要为自己的产品类型“贴上标签”(增加这个属性),剩下的交给创建者自己完成即可。

● 操作上更加简洁和统一,只使用一个BuildUp方法,构造出的产品类型在Builder<T>的类型参数中已经被定义,使用上客户程序代码也非常统一。

● 这里把BuildStepAttribute 封上了,实际项目中完全可以突破这个限制,把Times、Sequence通过配置文件告诉BuildStepAttribute。

不过从性能角度看,通过反射回调每个BuildPart()步骤似乎有些慢,而且每次BuildUp()的时候都需要通过反射动态获取IList<BuildStepAttribute>,这样似乎有些啰嗦。针对后者,可以参考Enterprise Library中自动发现存储过程参数列表的办法,增加一个缓冲,确保获取IList<BuildStepAttribute>的步骤仅被执行一次,其代码如下:

C#

private static IDictionary<Type, IList<BuildStepAttribute>> cache =
    new Dictionary<Type, IList<BuildStepAttribute>>();
protected virtual IList<BuildStepAttribute> DiscoveryBuildSteps()
{
    if (!cache.ContainsKey(typeof(T)))
    {
    // … …
    }
    return cache[typeof(T)];
}

很明显,上面的设计并不是线程安全的,实际项目中您可以把Cache 设计成一个独立的Singleton对象,由这个Singleton实例维护所有的Type/IList<BuildStepAttribute>对应关系,同时借助Singleton模式中介绍的线程安全Singleton实现即可。

6.4 具有装配/卸裁能力的Builder

上面介绍了一个创建者的工作——BuildUp()出一个产品类型实例,但实际应用中不仅仅是BuildU(),或者说当我们无需某个创建出的实例后,不一定单纯的product=null;就可以解决问题。比如:一个Database对象在被置为null之前最好关闭已经打开的连接,如果引用了一个Socket端口,也最好把它释放掉。

.NET和Java 都提供了垃圾回收机制,但最朴实的道理是“将欲取之,必先与之”,如果不是当前对象通过CLR 创建的托管对象,释放的时候就要格外小心了,尤其当涉及一些会引起争用的对象。我们之前说过,创建者模式处理的是“复杂对象”,这个对象很多时候是由很多子对象组成的,而且很有可能具有层次关系,为了更好地逆向处理掉BuildUp过程创建的各种资源,不妨参考 ObjectBuilder的方式在IBuilder的定义上增加一个TearDown()方法,向客户程序屏蔽目标对象的释放细节。

上面介绍的TearDown是“一次性处理”过程,有时候对象可能还有“部分处理”过程,按照设计模式的分类,这些一般会放到结构型或行为型模式中(非创建型模式),不过这里为了实现一个具有动态能力的创建者,把部分处理的TearDown放到了IBuilder的定义里。由于BuildUp()和TearDown()是成对的操作,因此,如果BuildUp()比较复杂,建议为其设计一个专用的TearDown()。下面我们针对这样一个“复杂”的类型设计具有闭合操作(BuildUp()& TearDown())的创建者,其数据类型结构如图6-5所示。

图6-5 示例的数据类型结构

其示例代码如下:

C# 定义产品类型

public class Product
{
    public int Count;
    public IList<int> Items;
}

C# 描述具有闭合操作的抽象创建者

public interface IBuilder<T>
{
    T BuildUp();
    T TearDown();
}

C#实现具有闭合操作的创建者

public class ProductBuilder : IBuilder<Product>
{
    private Product product=new Product();
    private Random random=new Random();
    public Product BuildUp()
    {
        product.Count=0;
        product.Items=new List<int>();
        for (int i=0; i < 5; i++)
        {
          product.Items.Add(random.Next());
          product.Count++;
        }
        return product;
    }
    public Product TearDown()
    {
        while (product.Count > 0)
        {
          int val=product.Items[0];
          product.Items.Remove(val);
          product.Count--;
        }
        return product;
    }
}

Unit Test

[TestMethod]
public void Test()
{
    IBuilder<Product> builder=new ProductBuilder();
    Product product=builder.BuildUp();
    Assert.AreEqual<int>(5, product.Count);
    Assert.AreEqual<int>(5, product.Items.Count);
    product=builder.TearDown();
    Assert.AreEqual<int>(0, product.Count);
    Assert.AreEqual<int>(0, product.Items.Count);
}

上面示例在TearDown()过程中处理的是简单的对象,但项目中操作的往往是类,尤其很可能是各种非托管的类。这些类同样可以在客户程序完成,但这样未免把客户程序与产品类型绑得太死了。在此还有个方案,就是在Product实现IDisposable的时候完成类似的操作,但就如前面所讲的,很多时候Product比较“复杂”,它不包括的子对象类型不好确定,单纯的IDisposable 不能解决“一大串”对象的清理工作,尤其有时候这一大串对象里有很多是“借用”的,不能随便被清理掉,此时采用TearDown()方法相当于给构造器设计者添加一个“优雅”清理过程的机会。

6.5 看着图纸加工——登记配置文件

6.5.1 把UML的对象变成XSD

与“相对简单”的工厂方法模式、单件模式和抽象工厂模式相比较,构造器模式似乎更适合把构造内容的登记情况保存在外部机制上,以便可以根据需要动态地把BuildPart()的工作通过外部机制告知创建者(或创建者自己去定位)。在这里我们先明确应用场景:

●对象是“复杂”的,以至于我们需要通过多个步骤来分别构造它,同时由于不同部分的组装过程又是相对稳定的,所以我们可以通过调度每个部分的组装过程,完成一个“复杂”目标实体的构造工作。也就是说,当审视自己设计的时候,看看创建者模式的上下文是否和设计中遇到的问题一致。

● 要考虑这个“复杂”对象是不是基于一个相对稳定的框架以控制反转或依赖注入的方式加入到主体框架运行的。

● 从软件生命周期上看,这种修改经常不是在开发期间,而是在运维过程中进行的。

如果上述三个条件同时得到满足,您可以考虑把如何创建这个对象的定义放在配置文件里了,不过如何设计这个对象就要看您的需要了。这里提示一个捷径——XSD,因为类图中的静态结构通过XSD可以比较好地过渡到配置文件中。例如图6-6这个类图表示的类型关系,通过XSD表示成:

图6-6 示例的层次型对象

其示例代码如下:

XSD

<?xml version="1.0" encoding="UTF-8"?>
… …
  <xs:element name="Product">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="Feature" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="Name" type="xs:string" use="required"/>
      <xs:attribute name="Price" type="xs:double"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="Feature">
    <xs:complexType>
      <xs:attribute name="Name" type="xs:string" use="required"/>
      <xs:attribute name="Description" type="xs:string"/>
    </xs:complexType>
  </xs:element>
</xs:schema>

对于开发人员而言,UML的类图可以相对清楚地告诉我们一个“复杂”对象的静态结构,XMI虽然在描述UML的时候看起来有点“形而上学”,但起码有了个大概的骨架,其实对于构造器而言,大体上它能做的也就是根据这个骨架把对象BuildUp()出来即可,至于每个BuildPart()步骤的内容则交给具体构造器类型来实现,或者借助本章前面的Attribute方式通过反射获得。

当设计配置文件时,我们并不需要完全照搬XMI的定义,仅需要在UML工具把类图导出为XMI描述后,把相对稳定的部分剔掉,仅保留需要的部分,然后按照.NET Framework提供的“4件套”配置对象定义方式在APP.Config(或Web.Config)中把它们描述出来,最后构造出对应的ConfigurationSection、ConfigurationElement,用于表示每个配置实体。下面我们看一个完整的示例过程。

与一般的开发无异,在此也就是设计接口、抽象类,毕竟如果为每个产品类型都定义配置类型,则成本太高,可能的话还是借助面向对象的抽象机制,先找到其共性。上面的IProduct接口描述的其实就是这类对象的抽象特征。

6.5.2 把握梗概——删除不经常变化的内容

从上面的设计看,Feature描述的其实就是一个“Name/Description”对,而对于IProduct而言,按照“依赖于抽象而非具体的原则”,IProduct能操作的最好就是基于Feature Element中描述的两个属性,在这里,Feature虽然可以被其子类继续继承,但从IProduct的角度看,它又是稳定的,所以可以从XSD中剔除(如果确实需要通过配置制定具体采用哪种Feature类型,最好另起一个配置节或配置元素单独定义),这样就把XSD中本应单独定义的Feature节点收缩到Product节点内部,其示例代码如下:

XSD

<?xml version="1.0" encoding="UTF-8"?>
… …
  <xs:element name="Product">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Features">
          <xs:complexType>
          <xs:sequence>
              <xs:element name="Feature" maxOccurs="unbounded">
                <xs:complexType>
                  <xs:attribute name="Name" type="xs:string" use="required"/>
                  <xs:attribute name="Description" type="xs:string"/>
                </xs:complexType>
              </xs:element>
          </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
      <xs:attribute name="Name" type="xs:string" use="required"/>
      <xs:attribute name="Price" type="xs:double"/>
    </xs:complexType>
  </xs:element>
</xs:schema>

6.5.3 映射为配置节点或配置元素

考虑到命名空间的安排,本章创建者模式启用的根配置节组节点为:

<marvellousWorks.practicalPattern.builder>

另外,考虑到创建者可以根据 XSD 创建各种产品类型,因此在根节点下定义了一个<customProducts >节点,作为所有基于配置动态创建产品的根配置元素集合节点。按照XSD的定义依次展开后就获得了如下配置文件结构:

App.Config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="marvellousWorks.practicalPattern.builder" type=" ">
      <section name="customProducts" type=""/>
    </sectionGroup>
  </configSections>
  <marvellousWorks.practicalPattern.builder>
    <customProducts>
      <concreteFeature type=""/>
      <products>
        <!--type为动态创建实现了IProduct接口的类型,采用qualified name形式-->
        <add name=" " price="" type=" ">
          <features>
          <add name=" " description=""/>
          </features>
        </add>
      </products>
    </customProducts>
  </marvellousWorks.practicalPattern.builder>
</configuration>

6.5.4实现实体对象

按照.NET Framework标准“4件套”的配置对象,依据App.Config的定义,我们设计了如图6-7所示的配置实体对象:

图6-7配置解析对象

考虑到ConfigurationElementCollection和ConfigurationElement都具有“name”属性,而且集合操作也很类似,所以增加了两个名为NamedConfigurationElement和NamedConfigurationElementCollection的基类。

其示例代码如下:

C# FeatureConfigurationElement

/// 代表某个具体的“product”配置元素
public class ProductConfigurationElement : NamedConfigurationElement
{
    [ConfigurationProperty("price", DefaultValue="0")]
    public virtual double Price { get { return (double)this["price"]; } }
    [ConfigurationProperty("type", IsRequired=true)]
    public virtual string TypeName { get { return (string)this["type"]; } }
    [ConfigurationProperty("features")]
    public FeatureConfigurationElementCollection Features
    {
        get { return base["features"] as
          FeatureConfigurationElementCollection; }
    }
    ///配置元素同时作为工厂,创建一个“没加雕琢”过的产品实例毛坯
    public IProduct Create()
    {
        return (IProduct)Activator.CreateInstance(Type.GetType(TypeName));
    }
}
/// 代表某个具体的“products”配置元素集合
  [ConfigurationCollection(typeof(ProductConfigurationElement),
CollectionType=ConfigurationElementCollectionType.AddRemoveClearMap)]
public class ProductConfigurationElementCollection :
    NamedConfigurationElementCollection<ProductConfigurationElement> { }
/// FeatureConfigurationElement类似ProductConfigurationElement

C#实体Feature类型

/// 代表实际需要使用的标示产品每个特性的实体类型
public class ConcreteFeatureConfigurationElement : ConfigurationElement
{
    [ConfigurationProperty("type", IsRequired=true)]
    public virtual string TypeName{ get { return (string)base["type"]; } }
    /// 创建一个产品特性类的实例
    public IFeature Create()
    {
        return (IFeature)Activator.CreateInstance(Type.GetType(TypeName));
    }
}

C#有关产品类型配置部分的根节点CustomProductConfigurationSection

public class CustomProductConfigurationSection : ConfigurationSection
{
    [ConfigurationProperty("concreteFeature")]
    public ConcreteFeatureConfigurationElement ConcreteFeature
    {
        get { return
          (ConcreteFeatureConfigurationElement)base["concreteFeature"]; }
    }
    [ConfigurationProperty("products")]
    public ProductConfigurationElementCollection Products
    {
        get { return
          (ProductConfigurationElementCollection)base["products"]; }
    }
}

C# 整个构造器部分的根节点BuilderConfigurationSectionGroup

///构造器模式所有配置内容的根配置节对象
public class BuilderConfigurationSectionGroup : ConfigurationSectionGroup
{
    public BuilderConfigurationSectionGroup() : base() { }
    [ConfigurationProperty("customProducts")]
    public CustomProductConfigurationSection CustomProducts
    {
        get { return base.Sections["customProducts"] as
          CustomProductConfigurationSection; }
    }
}

App.Config 填充了测试数据后的结果

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="marvellousWorks.practicalPattern.builder"
            type="…">
      <section name="customProducts" type="…"/>
    </sectionGroup>
  </configSections>
  <marvellousWorks.practicalPattern.builder>
    <customProducts>
      <concreteFeature type="…"/>
      <products>
        <add name="car" price="50000" type="…">
          <features>
          <add name="wheel" description="4"/>
          <add name="body" description="1"/>
          <add name="engine" description="1"/>
          </features>
        </add>
      </products>
    </customProducts>
  </marvellousWorks.practicalPattern.builder>
</configuration>

Unit Test

private const string groupName="marvellousWorks.practicalPattern.builder";
[TestMethod]
public void Test()
{
    Configuration configuration=ConfigurationManager.
      OpenExeConfiguration(ConfigurationUserLevel.None);
    BuilderConfigurationSectionGroup sectionGroup=(BuilderConfiguration-
      SectionGroup)configuration.GetSectionGroup(groupName);
    Assert.IsNotNull(sectionGroup);
    IFeature feature=sectionGroup.CustomProducts.ConcreteFeature.Create();
    Assert.IsNotNull(feature);
    string productTypeName =
      sectionGroup.CustomProducts.Products["car"].Type;
    IProduct product =
      (IProduct)Activator.CreateInstance(Type.GetType(productTypeName));
    Assert.IsNotNull(product);
}

6.5.5 完成流水线生产

使用上面准备好的配置对象,我们可以实现一个基于配置的创建者。这里为了不与经典创建者模式的差别过大,定义上还是增加了“中规中矩”的BuildPart()方法,其代码如下:

C# 基于配置的创建者

public interface IBuilder
{
    IProduct Create(string name);
    void BuildPart(FeatureConfigurationElement config);
}
public class ConcreteBuilder : IBuilder
{
    private IProduct product;
    public IProduct Create(string name)
    {
        if (string.IsNullOrEmpty(name)) throw new
          ArgumentNullException("name");
        ProductConfigurationElement config =
          ConfigurationBroker.GetConfiguration(name);
        product=config.Create();
        // 通过Setter注入的方式,按照配置将产品类型实例的内容填充完整
        product.Name=config.Name;
        product.Price=config.Price;
        if ((config.Features == null) || (config.Features.Count == 0)) return
          product;
        foreach (FeatureConfigurationElement featureConfig in
          config.Features)
          BuildPart(featureConfig);  // 具体创建步骤
        return product;
    }
    public void BuildPart(FeatureConfigurationElement config)
    {
        IFeature feature=ConfigurationBroker.CreateFeature();
        feature.Name=config.Name;
        feature.Description=config.Description;
        product.Features.Add(feature);
    }
}

使用基于配置的创建者还有一个用途,即解决非空构造函数、非空BuildPart()方法的问题,设计上可以把需要传递的参数写在配置文件里面,这样更加贴近我们的项目实际。比如在上面的示例中,你可以在每个对象下面增加一个名为<parameters>的配置元素集合,它向Activator传递CreateInstance()时需要的构造函数参数。

6.6 用迭代器控制流水线

很多时候您可能觉得采用实现基于Attribute的Builder过于麻烦,但根据上下文条件,BuildUp()和TearDown()在执行每个BuildPart()或TearDownPart()的时候,需要有不同的次序,而不是将次序彻底固定下来。

现实生活中这个情况很常见,以我们 BuildUp()自己为例。一开始大家可能都按部就班地求学、工作,但假如在大学里的某一天,您突然在实验室找到了一个自己认为非常有可能成功的大发现,开始准备创业,工作多年成功之后,您可能会选择再回到学校读个MBA什么的,总体而言,这些和您要达成的“复杂”的人生理想目标、方向是一致的,只不过道路不同;至于TearDown()的次序也有多种,而且很可能有几套预案,比如因为找了位太太,然后又当上父亲,所以您可能就要和之前经常出去玩的朋友分开了;因为要照顾家庭,当然也可能因为与游伴发生不愉快,所以就和大家分开了。总而言之,BuildUp()和TearDown()的次序经常需要留出“备选”的方案。开发的项目也是这样,比如应用处于Internet Ready的时候,构造过程除了new()和本地 BuildPart()之外,还要检查是不是有最新的更新,但在Internet Not Ready的时候可能就直接new()了。

抽象看待这些问题,我们发现关键是要增加一些机制来控制 BuildUp()和TearDown()中每个BuildPart和TearDownPart的次序。回想我们第1章的介绍,您会提出疑问,“为什么不用迭代器呢?”是的,这里就准备用迭代器控制 BuildUp()和TearDown()中每个步骤的执行次序。下面我们看一个示例:

假设我们回到大学,按照课表的要求,每周的学习应该是根据课表展开的,把这些课程逐个学过之后,相当于给自己执行了一遍BuildPart(),但学期末出现了什么情况呢?您突然发现对A、B、C三门课程的考试都没底气,但是从学分上来看,B>A>C,在临阵磨枪实在磨不过来的情况下,只好毅然决定“丢卒保车”了。下面我们看看“默认”情况下和“丢卒保车”情况下,制定的学习计划有何不同,其代码如下:

与之前设计不同的是,这里迭代器迭代的是一个个执行方法,而不是具体的数据。至于谁来管理这个迭代器,还是回归到经典的创建者模式,由Director来处理,因为在大多数情况下Director更多地作为一个流程的组织者。

C#抽象部分定义

/// 迭代的结果是这个委托所定义的抽象方法
public delegate string StudyHandler();
public interface IBuilder
{
    string StudyA();  // BuildPart()
    string StudyB();  // BuildPart()
    string StudyC();  // BuildPart()
}
public interface IDirector
{
    IEnumerable<StudyHandler> PlanSchedule(IBuilder builder);
    IList<string> Construct(IBuilder builder);  // 生成学习计划
}
public abstract class DirectorBase : IDirector
{
    public abstract IEnumerable<StudyHandler> PlanSchedule(IBuilder builder);
    public IList<string> Construct(IBuilder builder)
    {
        IList<string> schedule=new List<string>();
        foreach (StudyHandler handler in PlanSchedule(builder))
          schedule.Add(handler());
        return schedule;
    }
}

C#两个不同的计划

/// 按部就班的方式
public override IEnumerable<StudyHandler> PlanSchedule(IBuilder builder)
{
    yield return new StudyHandler(builder.StudyA);
    yield return new StudyHandler(builder.StudyB);
    yield return new StudyHandler(builder.StudyC);
}
/// 期末的方式
public override IEnumerable<StudyHandler> PlanSchedule(IBuilder builder)
{
    yield return new StudyHandler(builder.StudyB);
    yield return new StudyHandler(builder.StudyB);
    yield return new StudyHandler(builder.StudyA);
}

Unit Test

[TestMethod]
public void Test()
{
    IBuilder builder=new ConcreteBuilder();
    IDirector director=new FinalPhaseDirector();  // 期末的状态
    IList<string> schedule=director.Construct(builder);
    Assert.AreEqual<string>("B", schedule[0]);
    Assert.AreEqual<string>("B", schedule[1]);
    director=new NormalPhaseDirector();          // 平常状态
    schedule=director.Construct(builder);
    Assert.AreEqual<string>("A", schedule[0]);
    Assert.AreEqual<string>("B", schedule[1]);
}

“委托+迭代器”可以很好地把BuildPart()的步骤留给子类完成,而Director仅仅负责在更高层抽象部分“一股脑”地迭代执行就可以了。相对于经典方式下的Director而言,采用迭代器更容易把相关的执行内容组织起来,比如上例中只要是符合 StudyHandler的就可以。如果需要统一控制(比如学籍检查、学期检查等),可以在DirectorBase 动态增加,既“多态”又照顾到“集中”的需要,最起码子类可以不必重复地编写 foreach代码,只需把自己的执行计划按照迭代器的方式写出来即可。

6.7 小结

在整个创建型模式中,创建者模式往往被用来做一些“精细活”,它除了创建对象之外(new()的工作也可能通过某个工厂协助完成),还要负责把复杂的对象及其他的每个部分组装起来,由于“组装工艺”的不同,构造出来的产品可能完全不一样。

工程中,我们还进行了很多扩展:

●为了动态组织构造工艺,我们把每个加工步骤作了抽象,以异步动态组装的方式实现构造过程。

●为了不用一遍又一遍地实现IBuilder,我们给待构造目标对象的某些方法“贴上标签”,借助反射和Attribute,实现了一个具有动态发现机制的Builder。

● 考虑到构造出来的产品在处理前可能同样复杂,甚至比构造过程更复杂,我们实现了具有闭合操作(BuildUp()/TearDown())的Builder。

● 然后,考虑到部署和后续升级的问题,为了不打破应用的框架,又可以把繁琐的更新部署工作推给系统管理员,我们把“变化更频繁”的对象定义放到配置文件里。

● 最后,我们把构造次序本身抽象为一个迭代器,不同Director甚至同一个Director只要拿到这个迭代器就可以按照迭代器所说明的要求,依次执行每个加工步骤,而真正“做主”加工步骤的是IEnuerator<T>,只不过这里的T是个委托而不是某个数据类型。