首页  »   软件架构设计

生成器形式

网友分享于:2013-09-13  浏览:4次
生成器模式
实际上,既然Builder和Factory同属创建型模式,那么他们的最大共同点就在于都可以创建类对象, 在这点上,不光这两个模式一样,其它创建型模式也一样。但正如在《深入探索Factory模式与Prototype模式的异同(续)》一文中所说,这些模 式,功能上的相似,只是“形似而非神似”。既然这样,那好,下面就让我们能看看Builder和Factory在功能的相似上,存在哪些神韵方面的差别。

     首先,也是最重要的一点,就是虽然Builder和Factory都可创建产品,但两者所创建的产品类型完全不一样。Factory创建只能是单一的产品(单一在这指它非复合产品),而Builder所创建的产品是复合产品,即产品本身就是由其它部件产品组成的。

     举 个例子来说,现在要生产一辆车,假设它就只由这三个部分组成:玻璃、轮子、发动机。对于工厂模式来说,他创建后返回的,只能是玻璃,或者轮子,抑或是发动 机。不管怎么样,他不能向客户返回一辆完整的汽车,要得到一辆完整的汽车,客户必须自己动手去把这些零部件组装成一辆汽车。从这个意义上来讲,工厂模式中 的工厂,只是充当了零件厂的角色。那Builder又是如何创建产品的呢?在Builder模式中,一般不需要、也不充许向客户返回单个部件,他向客户返 回的,仅仅就是一部已经完全组装好的汽车成品。对于汽车部件的生产细节,客户不需要、也不应该让他们知道。写到这,我突然想到了组装电脑与品牌电脑的差 别,组装电脑虽然价格便宜,且易于改动,但性能没有保证,另外你自己还必须了解很多有关电脑方面的知识;对于品牌电脑,价格贵这点先暂时不说,关键在于他 不灵活,但是它的性能可以得到很好保证(由厂家),这易像我们在Builder的系统端保证部件的质量一样。另外,对于品牌电脑,客户根本不需要了解多少 电脑组装方面的知识,就可以把一台电脑抱回家,开机使用了。那么,在实际运用中,你是喜欢做DIY一族呢,还是喜欢稳定有保证的质量呢?好像在我们编著程 的这个过程中,我们比较趋向于使用“品牌电脑”。这也就为我们正确使用这两种设计模式提供了一个方向:如果你要生产的产品是由不同部件组成的,你最好使用Builder模式,而非Factory模式。

     另外,Builder和Factory的差别,就在于他们所生产部件 产品所在产品树的问题。这样说,可能有点拗口。具体来说吧,在工厂模式中,我们知道,一个工厂可以创建多个产品,但一个工厂模式中所创建的某个产品,都会 和另一个工厂中所创建的产品在同一棵继承树上。如果大家看过我最早写的《用Java实现的设计模式系列(1)—Factory 》那篇文章,就会记得,我在CFactoryMac中创建了一种产品叫MacRam,而在CFactoryWin中创建了另一种产品叫WinRam,很显 然,这两种产品是在同一棵继承树上的。对于它们之所以会出现在同一棵继承树上,是完全由Factory模式本身所决定的。大家如果看过Factory的 UMl图,就应该记得,要实现Factory模式,一定要有一个Abstract Product类,具体产品再由它派生出来。好了,说完了Factory,再让我们来看看Builder中是否必这么做!实际上,在Builder模式 中,我们只是在Abstract Builder中封装了创建部件的接口,而具体创建了什么部件,不同的实际Builder可能会生产出完全不一样的部件,这样不会存在任何问题,因为,我 上面说过,Builder只是向客户返回成品,而不向客户返回具体部件,这样,当然就允许产品的部件按要求随意变化了。

     再举个例子吧,假如你现在要创建两种风马不相及的东西,例如一种是人,它就只由这几部分组成:脑、身、四肢;另一种是树,也由三个部分组成:根、叶、茎。





当构造某对象时,也许无法保证总能事先获知对象的所有所需信息。尤其是,有时候 目标对象的构造器的参数只能逐步获取,我们希望能够一步一步地构造目标对象,常见情况比如解析器和用户接口。或者,当对类的重点难以很好把握并且类的构造 过程相当复杂时,你也许希望简化类的规模,这时就可以使用Builder模式。

Builder模式的意图是把构造对象实例的代码逻辑移到要实例化的类的外部。

1. 常见的生成器:
   使用Builder模式而获益的常见情况是定义所期望对象的数据被嵌套在文本字符串中。随着逐步查询代码或者说解析数据的过程中,你需要随着发现过程来保 存这些数据。不管解析器是基于XML的还是手工执行的,最初也许不足以拥有构造合法目标对象所需要的全部数据。对这种情况,Builder模式的处理方式 是把数据存储在临时对象中,直到程序拥有构造所需要的全部数据,这时候才查询存储的临时对象来构造目标对象。
   假设除了生产焰火制品之外,Oozinoz公司偶尔还对外提供焰火表演服务。旅行社可以按照下面的模板向Oozinoz公司发送电子邮件申请预定焰火表演:
  Date,November 5,Headcount,250,City,SprintField,
  DollarPerHead,9.95,HasSite,False
  可能你会猜到该协议一定是在XML之前诞生的。但是无论如何,该协议经实践证明的确是实用的。该预定请求说明预定焰火表演的时间和城市,以及最少来宾人数 和身强体壮来宾的服务费用。通过上面这封邮件,该旅行社告诉Oozinoz公司,将有250名来宾观看这次的焰火表演,并且该旅行社愿意为每位来宾支持 9。95美元,共计2487.50美元。另外,邮件还说明该旅行社还没有确定焰火表演的地点。
   我们当前的任务就是解析这个文本性请求并创建一个Reservation对象来表示这次预定申请。为此,我们可以先创建一个属性为空的 Reservation对象,并在解析器解析该文本请求的过程中逐步设置Reservation对象的各种属性。然而,这种做法存在这样一个问题:由此创 建的Reservation对象并不一定能代表一次有效的焰火预定申请。例如,当我们的解析器解析完文本请求之后,可能会发现该文本请求没有说明表演日 期。
   我们可以创建一个ReservationBuilder类,以保证所创建 的Reservation对象总能代表一次有效的焰火预定申请。该ReservationBuilder对象可以保存解析器解析出的各个预定请求属性,然 后再利用这些属性参数创建Reservation对象,随后验证该对象的有效性。下图给出了该设计所涉及的类。


      
     生成器类将某个领域的类的构造逻辑提取出来。每当解析器解析出一个初始化参数,
                                 就把该参数交给生成器类对象 

   ReservationBuilder类的build()方法是抽象方法,因而ReservationBuilder类是个抽象类。根据基于不完整数据创 建Reservation对象的方式不同,我们将构造非抽象的ReservationBuilder子类。ReservationParser类构造器把 生成器作为参数,并向之传递信息。parse()方法从预定字符串读取信息,并传递给生成器,代码如下所示:

public void parse(String s) throws ParseException
{
String[] tokens = s.split(",");
for(int i=0;i<tokens.length;i+=2)
{
String type = tokens[i];
String val = tokens[i+1];

if("date".compareToIgnoreCase(type) == 0)
{
Calendar now = Calendar.getInstance();
DateFormat formatter = DataFormat.getDateInstance();
Date d = formatter.parse( val + "," + now.get(Calendar.YEAR));
builder.setDate(ReservationBuilder.futurize(d));
}else if("headcount".compareToIgnoreCase(type) == 0)
builder.setHeadcount(Integer.parseInt(val));
else if("City".compareToIgnoreCase(type) == 0)
builder.setCity(val.trim());
else if("DollarPerHead".compareToIgnoreCase(type)==0)
builder.setDollarsPerHead(new Dollars(Double.parseDouble(val)));
else if("HasSite".compareToIgnoreCase(type)==0)
builder.setHasSite(val.equalsIgnoreCase("true"));
}
}

当发现"date"时,解析器就解析接下来的值,并把日期保存起来。futurize()方法把年份放在前面,这样可以保证日期"November 5"解析为11月5日。当读者查看代码时,也许会发现解析器可能会误入歧途,从预定字符串的最初标志位开始解析。

突破题:split(s)调用使用的正则表达式对象会把逗号分隔的列表分隔为多个独立的字符串。请考虑如何改进这种正则表达式或整个方法,保证解析器能够更好地识别预定信息。
答:让解析器更加灵活的一种方式是允许接收逗号后的多个空格。为实现这一点,split()调用模式如下:
s.split(",*");
或者通过像如下代码那样初始化Regex对象,可以得到任何类型的空格。
s.split(",\\s*");
\s字符表示正则表达式中的空格“字符类”。请注意,所有解决方案都假设这些字段内没有嵌套逗号。
  为了让这个正则表达式更加灵活,你也许会怀疑整个方法。尤其需要注意的是,你也许希望旅行代理能以XML格式来发送预定信息。你也许要建立一套标记,以便于XML解析器使用。

2.根据约束构建对象:
  在这个例子中,我们必须保证绝不会实例化一个无效的Reservation对象。具体而言,假定每个有效的焰火表演预定申请必须明确指出表演日期和城市。 另外,假定Oozinoz公司的商业规则规定每次焰火表演的观看人数必须大于或等于25人,或者总费用不得少于495.95美元。我们也许希望在数据库中 记录这些限制,但现在我们使用Java代码把这些信息记录为常量,代码如下所示:
public abstract class ReservationBuilder
{
    public static final int MINHEAD = 25;
    public static final Dollars MINTOTAL = new Dollars(495.95);
    //...
}

观看人数太少或者收入太少的预定申请也会被视为无效的。为了避免在预定申请无效 的情况下构造Reservation实例,我们可以在Reservation的构造器或者其调用的init()方法中加入进行商业规则检查的代码以及抛出 异常的代码。但是,一旦创建了Reservation对象之后,这些商业规则就不会再被使用,它与Reservation对象的其他方法没有任何瓜葛。这 个时候,我们可以创建一个生成器,并把Reservation的构造逻辑移入该生成器中。这样,Reservation类仅包含除构造之外的其他方法,从 而变得更加简洁。另外,通过使用该生成器,我们还可以对Reservation对象的不同参数进行验证,并对无效参数做出相应处理。最后,通过将构造逻辑 移入ResevationBuilder子类中,可以根据解析器解析出的参数逐步构造Reservation对象。下图给出了由 ReservationBuilder类派生出的两个非抽象子类,这两个子类对无效参数的处理方式不同。

            
        在根据给定的一组参数创建有效的对象的时候,生成器可能会遇到无效参数,这个
                             时候,有的生成器会抛出异常;有的生成器则会忽略
此图表更加清楚地说明了使用builder模式的好处:通过把构造逻辑和 Reservation类本身分离开,我们可以把构造过程作为一个独立的任务来实现,甚至可以创建独立的生成方法层次关系。生成器行为中的差异也许对预定 逻辑影响甚微。比如,上图的不同生成器区别在于是否抛出BuilderException异常。使用生成器的代码看起来如下代码所示:
package app.builder;
import com.oozinoz.reservation.*;

public class ShowUnforgiving
{
public static void main(String[] args)
{
  String sample = "Date,November 5,Headcount,250,"
                  +"City,Springfield,DollarsPerHead,9.95,"
  +"HasSite,False";
      ReservationBuilder builder = new UnforgivingBuilder();

  try
  {
new ReservationParser(builder).parse(sample);
Reservation res = builder.build();
System.out.println("Unforgiving builder:"+res);
  }
  catch (Exception e)
  {
  System.out.println(e.getMessage());
  }
}
}

运行上述代码会输出一个Reservation对象:
Date,November 5,Headcount,250,City,SprintField,
Dollar/Head,9.95,Has Site,false

上面这个应用程序首先给定了一个预定请求字符串,接着实例化了一个生成器和一个解析器,随后开始利用解析器解析该字符串。读入该预定的请求字符串之后,该解析器便开始不断地将解析出来的预定申请信息通过生成器的set方法传给生成器。
    预定请求字符串解析完毕之后,应用程序便用该生成器构造一个有效的预定对象。当出现异常的时候,该应用程序仅打印出该异常有关的文字信息。而在实际的应用中,当出现异常的时候,我们需要完成一些重要的异常处理工作。

突破题:当预定申请信息中的日期或者城市属性为空,或者观看人数太少,或者焰火表演的整场演出费用太低时,UnforgivingBuilder类的build()方法就会抛出BuilderException异常。请据此说明写出build()方法的实现。
答:若所有属性都是有效的,则build()方法就会返回有效的Reservation对象;否则,该方法就会抛出异常。下面是该方法的一种实现:
public Reservation build() throws BuilderException
{
if(date == null)
throw new BuilderException("Valid date not found");

if(city == null)
throws new BuilderException("Valid city not found");

if(headCount < MINHEAD)
throws new BuilderException("Minimum headcount is:"+ MINHEAD);

if(dollarsPerHead.times(headcount).isLessThen(MINTOTAL))
throws new BuilderException("Mininum total cost is" + MINTOTAL);

return new Reservation(
date,
headCount,
city,
dollarsPerHead,
hasSite);
}

ReservationBuilder超类定义常量MINHEAD和MINTOTAL。
  如果这个生成器没有遇到问题,则会返回一个有效的Reservation对象。

3.根据不完整信息构造符合约束的对象:
  UnforgivingBulder类将拒绝任何信息不完整的请求。公司可能会期望在客户的预定申请缺少某些信息的情况下,我们的软件系统能够对该系统进行适合的修改。
  具体而言,假定申请中没有指明观看焰火的人数,分析人员会要求我们根据公司的商业规则为观看人数设置一个最小值。同样,如果预定申请中没有指明焰火表演的 单人费用,我们可以为之设置一个合适的费用,从而使得整场演出费达到该商业规则指定的最小值。这些需求相当简单,但是设计起来需要一些技巧。比如,如果预 定字符串提供单人费用数据值,但是没有提供总人数,生成器应该怎么办?

突破题:请写出ForgivingBuilder.build()方法的规范,说明当预定字符串没有提供总人数或者单人费用时,生成器应该怎么办?
答:像以前一样,如果预定活动没有指定城市或者日期,则程序会抛出异常,因为无法预测这些数据。如果缺少总人数或者人均费用,请注意以下几点:
(1)如果预定请求没有说明总人数和人均费用,则程序会自动把总人数设置为最小值,把人均费用设置为最小总费用除总人数。
(2)如果预定请求没有说明总人数,但是指定了人均费用值,则程序会把总人数自动设置为最小值,保证总费用足够维持本次活动。
(3)如果预定请求指定了总人数,但是没有指定人均费用,则程序会把人均费用设置为某个值,保证总费用足够维护本次活动。
突破题:请写出ForgivingBuilder类中build()方法的实现。

public Reservation build() throws BuilderException
{
boolean noHeadCount = (headCount == 0);
boolean noDollarsPerHead = (dollarsPerHead.isZero());

if(noHeadcount && noDollarsPerHead)
{
headCount = MINHEAD;
dollarsPerHead = sufficientDollars(headCount);
}else if(noHeadCount)
{
headCount = (int)Math.ceil(MINTOTAL.divideBy(dollarsPerHead));
headCount = Math.max(headcount,MINHEAD);
}else if(noDollarsPerHead)
{
dollarsPerHead = sufficientDollars(headCount);
}

check();

return new Reservation(
date,
headCount,
city,
dollarsPerHead,
hasSite);
}
上述代码依赖于check()方法,这个方法类似于UnforgivingBuilder类的build()方法。
protected void check() throws BuilderException
{
if(date == null)
throw new BuilderException("Valid date not found");

if(city == null)
throw new BuilderException("Valid city not found");

if(headcount<MINHEAD)
throw new BuilderException("Minimum headcount is "+MINHEAD);

if(dollarsPerHead.times(headcount).isLessThan(MINTOTAL))
throw new BuilderException("Minimum total cost is "+MINTOTAL);
}
ForgivingBuilder类和UnforginvBuilder类可以确保Reservation对象始终有效。当构造预定对象出现问题时,你的设计也应该提供足够的灵活性来解决出现的问题。

4.小结: 
  Builder模式将一个复杂对象的构造逻辑从其代码中分离出来。其直接的效果就是简化了原来复杂的目标对象。生成器类集中负责目标类对象的构造,而目标 类则集中完成有效实例的各种非构造操作。其中模式的一个突出优点体现在,我们在实例化目标类之前可以能够构造一个有效的对象,而且不必将这些构造逻辑放在 目标类的构造器中。另外,Builder模式还使得我们可以逐步构造目标类对象。这个特点使得Builder模式特别知县于通过解析文本获取对象信息,或 者从图形用户界面收集对象信息来创建对象的场合。



相关解决方案

最新解决方案