java设计模式-建造者模式(Builder)

定义

建造模式是对象的创建模式。建造模式可以将一个产品的内部表象(internal representation)与产品的生产过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。

产品的内部表象

一个产品常有不同的组成成分作为产品的零件,这些零件有可能是对象,也有可能不是对象,他们通常又称为产品的内部表象(internal representation)。

不同的产品可以有不同的内部表象,也就是不同的零件。使用建造模式可以使客户端不需要知道所生产的产品有哪些零件,每个产品的对应零件有何不同,是如何建造出来的,以及如何组成产品。

对象性质的建造

有些情况下,一个对象会有一些重要的性质,在它们没有恰当的值之前,对象不能作为一个完整的产品使用。比如:一个电子邮件有发件人地址、收件人地址、主题、内容、附件等部分,而在最基本的发件人地址得到赋值之前,这个电子邮件是不可以发送的。

有些情况下,一个对象的有些性质必须按照某个顺序赋值才有意义。在某个性质没有赋值之前,另一个性质则无法赋值。这些情况使得性质本身的建造设计到复杂的商业逻辑。设置后,此对象相当于一个有待建造的产品,而对象的这些性质相当于产品的零件,建造产品的过程是建造零件的过程。由于建造零件的过程很复杂,因此,这些零件的建造过程往往被“外部化”到另一个成为建造者的对象中,建造者对象返还给客户端的是一个全部零件都建造完毕的产品对象。

建造模式利用一个导演者对象和具体建造者对象一个个的建造出所有的零件,从而建造出完整的产品对象。建造者模式将产品的结构和产品的零件的建造过程对客户端隐藏起来,把对建造过程进行指挥的责任和具体建造者零件的责任分割开来,达到责任划分和封装的目的。

建造模式的结构

建造模式的结构

在这个示意性的系统里,最终产品Product只有两个零件,即part1part2。相应的构造方法也有两个:buildPart1()buildPart2()。同时可以看出本模式涉及到四个角色,他们分别为:

  • 抽象建造者(Builder):给出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口独立于应用程序的商业逻辑。模式中直接创建产品对象的是具体建造者ConcreteBuilder角色。具体建造者类必须实现这个接口要求的两种方法:一种是建造方法,buildPart1()buildPart2();另一种是返回结构方法retrieveResult()。一般来说,产品所包含的零件数目与建造方法的数目相符。换言之,有多少零件需要建造,就会有多少相应的建造方法。
  • 具体建造者(ContreteBuilder):担任这个角色的是与应用程序紧密相关的一些类,他们在应用程序调用下创建产品的实例。这个角色要完成的任务包括:
    1. 实现抽象建造者Builder所声明的接口,给出一步步完成创建产品实例的操作。
    2. 在建造过程完成后,提供产品的实例。
  • 导演者(Director):担任这个角色的类调用具体建造者角色以创建产品对象。应当指出的是,导演者角色并没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者角色。
  • 产品(Product):产品便是建造中的复杂对象,一半来说,一个系统中会有多于一个的产品类,而且这些产品类并不一定有共同的借口,而完全可以是不相关联的。

导演者角色是与客户端打交道的角色,导演者将客户端创建产品的请求划分为对各个零件的建造请求,再将这些请求委派给具体建造者角色。
具体建造者角色是做具体建造工作的,但是却对客户端透明。
一般来说,每有一个产品类,就有一个相应的具体建造者类。这些产品应当有一样数目的零件,而每有一个零件就相应的在所有的建造者角色中有一个建造方法。

示例代码

产品类Product

public class Product {
    /**
     * 产品零件
     */
    private String part1;
    private String part2;
    
    public String getPart1() {
        return part1;
    }
    public void setPart1(String part1) {
        this.part1 = part1;
    }
    public String getPart2() {
        return part2;
    }
    public void setPart2(String part2) {
        this.part2 = part2;
    }
    
    @Override
    public String toString() {
        return "Product [part1=" + part1 + ", part2=" + part2 + "]";
    }
}

抽象建造者接口Builder

/**
 * 抽象建造者角色
 * 
 * 提供零件建造方法及返回结果方法
 */
public interface Builder {
    void buildPart1();
    void buildPart2();
    
    Product retrieveResult();
}

具体建造者角色类ConcreteBuilder

/**
 * 具体建造者角色
 */
public class ConcreteBuilder implements Builder {
    
    private Product product = new Product();
    
    /**
     * 建造零件1
     */
    @Override
    public void buildPart1() {
        product.setPart1("编号:9999");
    }

    /**
     * 建造零件2
     */
    @Override
    public void buildPart2() {
        product.setPart2("名称:建造攻城狮");
    }

    /**
     * 返回建造后成功的产品
     * @return
     */
    @Override
    public Product retrieveResult() {
        return product;
    }

}

导演者角色类

/**
 * 导演者角色
 */
public class Director {
    /**
     * 创建建造者对象
     */
    private Builder builder;
    
    /**
     * 构造函数,给定建造者对象
     * @param builder 建造者对象
     */
    public Director(Builder builder) {
        this.builder = builder;
    }
    
    /**
     * 产品构造方法,在该方法内,调用产品零件建造方法。
     */
    public void construct(){
        builder.buildPart1();
        builder.buildPart2();
    }
}

客户端类Client

/*
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        //创建具体建造者对象
        Builder builder = new ConcreteBuilder();
        //创造导演者角色,给定建造者对象
        Director director = new Director(builder);
        //调用导演者角色,创建产品零件
        director.construct();
        //接收建造者角色产品建造结果
        Product product = builder.retrieveResult();
        System.out.println(product.toString());
    }
}

客户端创建具体建造者对象,然后将具体建造者对象交给导演者角色,导演者操作建造者对象建造产品零件,当产品创建完成后,建造者将产品返回给客户端。

将创建具体建造者对象的任务交给客户端,而不是导演者对象的原因,是为了将导演者对象同具体建造者对象的耦合变成动态的,从而使得导演者对象可以操作多个具体建造者对象中的任何一个。

使用场景

假设有一个电子杂志系统,定期的向用户的电子邮箱发送电子杂志,用户可以通过网页订阅电子杂志,也可以通过网页结束订阅。

当用户开始订阅时,系统发送一个电子邮件表示欢迎,当客户结束订阅时,系统发送一个电子邮件表示欢送。

本例子就是这个系统中负责发送“欢迎”和“欢送”邮件的模块。

本例子中,产品类就是发送给某个客户的邮件,如下图所示

邮件组成图

虽然在这个例子里面每个产品类均有一个共同的接口,但这仅为本例子特有的,并不代表建造者模式的特点。
建造者模式可以应用到具有完全不同接口的产品类上。大多数情况下是不知道最终构建出来的产品是什么样的,所以在标准的建造者模式里面,一般对产品是不需要定义抽象接口的,因为最终建造的产品完全不一样,给这些产品定义公共抽象接口几乎是没有任何意义的。

下图所示就是这个系统的类图:

系统类图

这个系统包含有客户端(Client)导演者(Director)抽象建造者(Builder)具体建造者(WelcomeBuilder和GoodbyeBuilder)产品(WelcomeMessage和GoodbyeMessage)等角色。

示例代码

抽象类AutoMessage源代码,send()方法在此处为测试方法,不进行正式发邮件操作。

import java.util.Date;

public abstract class AutoMessage {
    /**
     * 收件人地址
     */
    private String to;
    /**
     * 发件人地址
     */
    private String from;
    /**
     * 标题
     */
    private String subject;
    /**
     * 内容
     */
    private String body;
    /**
     * 发送日期
     */
    private Date sendDate;
    
    public void send() {
        System.out.println("收件人地址:" + to);
        System.out.println("发件人地址:" + from);
        System.out.println("标题:" + subject);
        System.out.println("内容:" + body);
        System.out.println("发送日期:" + sendDate);
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public Date getSendDate() {
        return sendDate;
    }

    public void setSendDate(Date sendDate) {
        this.sendDate = sendDate;
    }
}

具体欢迎产品类WelcomeMessage

public class WelcomeMessage extends AutoMessage {
    /**
     * 构造函数
     */
    public WelcomeMessage() {
        System.out.println("发送欢迎信息");
    }
}

具体欢送产品类GoodbyeMessage

public class GoodbyeMessage extends AutoMessage {
    /**
     * 构造函数
     */
    public GoodbyeMessage() {
        System.out.println("发送欢送信息");
    }
}

抽象建造者类Builder

import java.util.Date;

public abstract class Builder {
    protected AutoMessage message;
    /**
     * 建造标题零件的方法
     */
    public abstract void buildSubject();
    /**
     * 建造内容零件的方法
     */
    public abstract void buildBody();
    /**
     * 建造收件人零件
     */
    public void buildTo(String to) {
        message.setTo(to);
    }
    /**
     * 建造发件人零件
     */
    public void buildFrom(String from) {
        message.setFrom(from);
    }
    /**
     * 建造发送时间零件
     */
    public void buildSendDate() {
        message.setSendDate(new Date());
    }
    
    /**
     * 邮件构建完成后,调用方法发送邮件
     * 相当于建造者角色产品返回方法。
     */
    public void sendMessge() {
        message.send();
    }
}

具体欢迎建造者类WelcomeBuilder

public class WelcomeBuilder extends Builder {
    public WelcomeBuilder() {
        message = new WelcomeMessage();
    }
    
    @Override
    public void buildSubject() {
        message.setSubject("欢迎标题");
    }

    @Override
    public void buildBody() {
        message.setBody("欢迎内容");
    }

}

具体欢送建造者类GoodbyeBuilder

public class GoodbyeBuilder extends Builder {
    public GoodbyeBuilder() {
        message = new GoodbyeMessage();
    }

    @Override
    public void buildSubject() {
        message.setSubject("欢送标题");
    }

    @Override
    public void buildBody() {
        message.setBody("欢送正文");
    }
}

导演者Director,这个类提供一个construct()方法,此方法调用建造者的建造方法,包括各个零件的建造方法,从而逐个零件的建设对象。

public class Director {
    Builder builder;
    public Director(Builder builder) {
        this.builder = builder;
    }
    
    public void construct(String toAddress, String fromAdress) {
        this.builder.buildTo(toAddress);
        this.builder.buildFrom(fromAdress);
        this.builder.buildSubject();
        this.builder.buildBody();
        this.builder.buildSendDate();
    }
}

客户端Client

public class Client {
    public static void main(String[] args) {
        Builder builder = new GoodbyeBuilder();
        Director director = new Director(builder);
        director.construct("to@126.com", "from@126.com");
        builder.sendMessge();
    }
}

执行结果为:

发送欢送信息
收件人地址:to@126.com
发件人地址:from@126.com
标题:欢送标题
内容:欢送正文
发送日期:Thu Jun 22 18:14:55 CST 2017

建造者模式中有两个非常重要的部分:

  1. 一个部分是Builder接口,也就是建造者结构。这里定义了如何构建各个部件,也就是知道每个部件功能如何实现,以及如何装配这些部件到产品中去。
  2. 另外一个部分是Director导演者角色。导演者知道如何组合来建造产品,也就是Director负责产品的整体构建,通常是分步骤的来执行。

不管如何变化,建造模式都存在这么两个部分,一个部分是部件构造方法和产品装配,另一个部分是整体构建的算法。认识这点是很重要的,因为在建造者模式中,强调的是固定整体构建的算法,而灵活扩展和切换部件的是具体建造者角色和产品装配的方式。

也就是说,建造者模式的中心在于分离构造算法和具体的构造实现,从而使得建造算法可以重用,具体的构造实现可以很方便的进行拓展和切换,从而可以灵活的组合建造不同的产品对象。

使用建造者模式构建复杂对象

考虑这样一个世纪应用,要创建一个保险合同的对象,里面很多属性的值都有约束,要求创建出来的对象是满足这些约束规则的。

约束规则例如:保险合同通常情况下可以和个人签订,也可以和某个公司签订个,但是一份保险合同不能同时和个人和公司签订。这个对象里有很多类似于这样的约束,采用建造者模式来构建复杂的对象,通常会对建造者模式进行一定的简化,因为目标明确,就是创建某个复杂对象,因此做适当的简化会使得程序更简介。

大致简化如下:

  • 由于是用Builder建造者模式来创建某个对象,因此就没有必要再定义一个Builder接口,直接提供一个具体的建造类就可以了。
  • 对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉导演者Director,把导演者的功能和Client客户端的功能合并起来,也就是说Client客户端的功能就相当于导演者,它来指导建造者去构建需要的复杂对象。

保险合同及建造者角色类

/**
 * 保险合同编号
 */
public class InstranceContract {
    /**
     * 保险合同编号
     */
    private String contractId;
    /**
     * 受保人名称,此处因为有限制条件:要么同个人签订,要么同公司签订
     * 也就是说,受保人名称属性同受保公司名称属性不能同时有值。
     */
    private String personName;
    /**
     * 受保公司名称
     */
    private String companyName;
    /**
     * 开始时间
     */
    private long beginDate;
    /**
     * 结束时间,需要大于开始时间
     */
    private long endDate;
    /**
     * 其他数据
     */
    private String otherData;

    private InstranceContract(ConcreteBuilder builder){
        this.contractId = builder.contractId;
        this.personName = builder.personName;
        this.companyName = builder.companyName;
        this.beginDate = builder.beginDate;
        this.endDate = builder.endDate;
        this.otherData = builder.otherData;
    }
    
    /**
     * 保险合同的一些操作
     */
    public void someOperation(){
        System.out.println("当前正在操作的保险合同编号为【"+this.contractId+"】");
        System.out.println(toString());
    }
    
    @Override
    public String toString() {
        return "InstranceContract [contractId=" + contractId + ", personName=" + personName + ", companyName="
                + companyName + ", beginDate=" + beginDate + ", endDate=" + endDate + ", otherData=" + otherData + "]";
    }

    public static class ConcreteBuilder {
        private String contractId;
        private String personName;
        private String companyName;
        private long beginDate;
        private long endDate;
        private String otherData;
        
        /**
         * 构造方法
         * @param contractId 保险合同编号
         * @param beginDate 生效时间
         * @param endDate 失效时间
         */
        public ConcreteBuilder(String contractId, long beginDate, long endDate) {
            this.contractId = contractId;
            this.beginDate = beginDate;
            this.endDate = endDate;
        }
        
        public ConcreteBuilder setPersonName(String personName) {
            this.personName = personName;
            return this;
        }
        
        public ConcreteBuilder setCompanyName(String companyName) {
            this.companyName = companyName;
            return this;
        }
        
        public ConcreteBuilder setOtherData(String otherData) {
            this.otherData = otherData;
            return this;
        }
        
        public InstranceContract build() {
            if (contractId == null || contractId.trim().length() == 0) {
                throw new IllegalArgumentException("合同编号不能为空");
            }
            
            boolean signPerson = (personName != null && personName.trim().length() > 0);
            boolean signCompany = (companyName != null && companyName.trim().length() > 0);
            
            if (signPerson && signCompany) {
                throw new IllegalArgumentException("一份保险合同不能同时与个人和公司签订");
            }
            
            if (!signPerson && !signCompany) {
                throw new IllegalArgumentException("一份保险合同不能没有签订对象");
            }
            
            if (beginDate <= 0) {
                throw new IllegalArgumentException("一份保险合同必须有生效的日期");
            }
            
            if (endDate <= 0) {
                throw new IllegalArgumentException("一份保险合同必须有失效的日期");
            }
            
            if (endDate <= beginDate) {
                throw new IllegalArgumentException("一份保险合同的失效日期必须要大于生效的日期");
            }
            
            return new InstranceContract(this);
        }
    }
}

客户端

public class Client {
    public static void main(String[] args) {
        InstranceContract.ConcreteBuilder builder = 
                new InstranceContract.ConcreteBuilder("8888", 1233L, 2253L);
        
        InstranceContract contract = 
                builder.setPersonName("赵四").setOtherData("测试数据").build();
        
        contract.someOperation();
    }
}

在本例子中,我们将建造者角色合并到产品对象中,作为产品对象的内部类来使用。同时,我们将产品的构造函数私有化,防止用户直接使用构造函数创造对象。因为在这个例子中,建造者角色的主要目的就是构建合同对象,因此,可以不用创建单独的类。


在什么情况下使用建造者模式

  1. 需要生成的产品对象有复杂的内部结构,每一个内部成分本身也可以是对象,也可以仅仅是一个对象(产品)的一个组成部分。
  2. 需要生成的产品对象的属性相互依赖。建造模式可以强制实行一种分步骤进行的建造过程。因此,如果产品对象的一个属性必须在另外一个属性赋值之后才可以被赋值,那么,使用建造者模式是一个很好的设计思想。
  3. 在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到。

参考

《JAVA与模式》之建造模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容