您的位置:首页 > 产品设计 > UI/UE

Builder(生成器)模式

2009-05-15 21:22 246 查看

  当构造某对象时,也许无法保证总能事先获知对象的所有所需信息。尤其是,有时候目标对象的构造器的参数只能逐步获取,我们希望能够一步一步地构造目标对象,常见情况比如解析器和用户接口。或者,当对类的重点难以很好把握并且类的构造过程相当复杂时,你也许希望简化类的规模,这时就可以使用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模式特别知县于通过解析文本获取对象信息,或者从图形用户界面收集对象信息来创建对象的场合。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: