技术开发 频道

基于反射和属性的Builder模式实现


经典Builder模式
 

    创建型模式中,Builder模式一般用来处理“复杂”对象创建的,与Factory Method、Abstract Factory不同,它不仅要完成new()的工作,由于“复杂”对象还很有可能包括很多部件,因此Builder还需要进行每个部分的组装工作。《设计模式》中对
该模式表述为:

    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》

很大程度上这个意图太过精炼以至于学习者很不好把握它的要求,实质上创建者模式解决的问题是产品局部加工过程变化较大,但组装过程相对固定这类问题的处理。例如:
 一个最直接的例子就是“装”PC机,虽然声卡、显卡、内存、硬盘、机箱、键盘鼠标、显示器都有很多的不同,无论是图形工作站还是一般娱乐用的笔记本,无论是品牌机还是自己“攒”的机器,每个部分的制造和生产过程都有很多不同,但组装一台计算机的过程相对固定;
 一辆汽车也是,虽然不同品牌不同档次的汽车在轮胎、发动机、车身的制作工艺上又很大区别,但组装的过程相对而言也非常稳定;

    上面这些例子就是创建者模式一个典型的应用情景,经典的创建者模式静态结构如下:



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

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

优势:

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

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

 构造过程的最终成果可以根据实际变化的情况,选择使用一个统一的接口,或者是不同类的对象,给客户类型更大的灵活度;

劣势:

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


新的思路
 

    回想一下,现实中我们组装一台机器或者组装一部汽车的时候,经常的办法是检查产品自己的说明书,然后开始执行构造的工作。对于我们待加工的对象是否也可以考虑类似的办法呢?可以,不过可能代价比较大,比如让每个产品都自己实现一个IList<BuildStepHandler>的属性(这里BuildStepHandler是委托,它其实指向每个具体的BuildPart()方法),但这么做太麻烦。

    .NET平台有个很好的机制——“贴标签”,也就是用Attribute来表示扩展目标产品类型的创建信息,这样我们就可以完成一个非常通用的Builder类,由它“看着说明书”加工多种多样的产品。 

完成工具类型准备:
    由于读取属性一般通过反射获得,我们先做个一个新工具——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 (GetMethodCustomAttributes<T>(method) != null)
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];
}

    有了这个工具类之后,我们就可以设计指导Builder进行产品类型创建的属性了。下面是一个示例的静态结构:

    (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); // 实际只执行了两个方法, 但3次调用
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<BuildStepAttbite>似乎也太啰嗦。针对后者这个问题可以参考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>对应关系) 。

总结:

    在整个创建型模式中,创建者模式往往被用来做一些“精细活”,他除了创建对象之外还要负责把复杂的对象及其他的每个部分组装起来,由于“组装工艺”的不同,构造出来的产品可能完全不一样。但工程中,我们还有很多扩展,比如本文为了不用一遍又一遍的实现IBuilder,我们给待构造目标对象的某些方法“贴上标签”,借助反射和Attribute,我们实现了一个具有动态发现机制的Builder。
0
相关文章