场景引入:无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如汽车,它包括车轮、方向盘、发送机等各种部件。而对于大多数用户而言,无须知道这些部件的装配细节,也几乎不会使用单独某个部件,而是使用一辆完整的汽车,可以通过建造者模式对其进行设计与描述,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节.
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
建造者模式是一种对象创建型模式,他将客户端与包含多个部件的复杂对象的创建过程分离,客户端无需知道复杂对象的内部组成部分与装配方式,只需知道所建造者的类型即可。
建造者模式结构图
由上图可知,建造者模式包含以下4个角色
(1)Builder(抽象建造者):它为创建产品对象的各个部件指定抽象借口,在该接口中一般声明两种方法:一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口
(2)ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确所创建的复杂对象,还可以提供一个返回创建好的复杂产品对象
(3)Product(产品):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程
(4)Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
建造者模式应用实例
1. 实例说明
某游戏软件公司决定开发一款基于角色扮演的多人在线网络游戏,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(例如力量、魔法、技能等)具有不同的能力,角色也会随着不断升级而拥有更加强大的能力。作为该游戏的一个重要组成部分,而且随着该游戏的升级将不断增加新的角色。通过分析发现,游戏角色是一个复杂对象,它包含性别、脸型等多个组成部分,不同类型的游戏角色,其性别、脸型、服装、发型等外部特征都有所差异,例如“天使”拥有美丽的面容和披肩长发,并身着一袭白裙;而“恶魔”极其丑陋,留着光头并穿着一件刺眼的黑衣。
2. 实例类图
3. 实例代码
1 package designpatterns.builder; 2 3 public class Actor { 4 private String type; //角色类型 5 private String sex; //性别 6 private String face; //脸型 7 private String costume; //服装 8 private String hairstyle; //发型 9 public String getType() { 10 return type; 11 } 12 public String getSex() { 13 return sex; 14 } 15 public String getFace() { 16 return face; 17 } 18 public String getCostume() { 19 return costume; 20 } 21 public String getHairstyle() { 22 return hairstyle; 23 } 24 public void setType(String type) { 25 this.type = type; 26 } 27 public void setSex(String sex) { 28 this.sex = sex; 29 } 30 public void setFace(String face) { 31 this.face = face; 32 } 33 public void setCostume(String costume) { 34 this.costume = costume; 35 } 36 public void setHairstyle(String hairstyle) { 37 this.hairstyle = hairstyle; 38 } 39 }
1 package designpatterns.builder; 2 3 public abstract class ActorBuilder { 4 protected Actor actor = new Actor(); 5 6 public abstract void buildType(); 7 public abstract void buildSex(); 8 public abstract void buildFace(); 9 public abstract void buildCostume(); 10 public abstract void buildHairstyle(); 11 12 public Actor createActor(){ 13 return actor; 14 } 15 }
1 package designpatterns.builder; 2 3 public class HeroBuilder extends ActorBuilder { 4 5 @Override 6 public void buildType() { 7 actor.setType("英雄"); 8 } 9 10 @Override 11 public void buildSex() { 12 actor.setSex("男"); 13 } 14 15 @Override 16 public void buildFace() { 17 actor.setFace("英俊"); 18 } 19 20 @Override 21 public void buildCostume() { 22 actor.setCostume("盔甲"); 23 } 24 25 @Override 26 public void buildHairstyle() { 27 actor.setHairstyle("飘逸"); 28 } 29 30 }
1 package designpatterns.builder; 2 3 public class AngelBuilder extends ActorBuilder { 4 5 @Override 6 public void buildType() { 7 actor.setType("天使"); 8 } 9 10 @Override 11 public void buildSex() { 12 actor.setSex("女"); 13 } 14 15 @Override 16 public void buildFace() { 17 actor.setFace("漂亮"); 18 } 19 20 @Override 21 public void buildCostume() { 22 actor.setCostume("白裙"); 23 } 24 25 @Override 26 public void buildHairstyle() { 27 actor.setHairstyle("披肩长发"); 28 } 29 30 }
1 package designpatterns.builder; 2 3 public class DevilBuilder extends ActorBuilder { 4 5 @Override 6 public void buildType() { 7 actor.setType("恶魔"); 8 } 9 10 @Override 11 public void buildSex() { 12 actor.setSex("妖"); 13 } 14 15 @Override 16 public void buildFace() { 17 actor.setFace("丑陋"); 18 } 19 20 @Override 21 public void buildCostume() { 22 actor.setCostume("黑衣"); 23 } 24 25 @Override 26 public void buildHairstyle() { 27 actor.setHairstyle("光头"); 28 } 29 30 }
1 package designpatterns.builder; 2 3 public class ActorController { 4 public Actor construct(ActorBuilder ab){ 5 Actor actor; 6 ab.buildType(); 7 ab.buildSex(); 8 ab.buildFace(); 9 ab.buildCostume(); 10 ab.buildHairstyle(); 11 actor = ab.createActor(); 12 return actor; 13 } 14 }
1 <?xml version="1.0"?> 2 <config> 3 <className>designpatterns.builder.AngelBuilder</className> 4 </config>
1 package designpatterns.builder; 2 3 import java.io.File; 4 import javax.xml.parsers.*; 5 import org.w3c.dom.*; 6 7 public class XMLUtil { 8 public static Object getBean() { 9 try { 10 //创建DOM文件对象 11 DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance(); 12 DocumentBuilder builder = dFactory.newDocumentBuilder(); 13 Document doc; 14 doc = builder.parse(new File("src//designpatterns//builder//config.xml")); 15 16 //获取包含类名的文本节点 17 NodeList nList = doc.getElementsByTagName("className"); 18 Node classNode = nList.item(0).getFirstChild(); 19 String cName = classNode.getNodeValue(); 20 21 //通过类名生成实例对象并将其返回 22 Class c = Class.forName(cName); 23 Object obj = c.newInstance(); 24 return obj; 25 } catch (Exception e) { 26 e.printStackTrace(); 27 return null; 28 } 29 } 30 }
1 package designpatterns.builder; 2 3 public class Client { 4 5 public static void main(String[] args) { 6 ActorBuilder ab; 7 ab = (ActorBuilder)XMLUtil.getBean(); 8 9 ActorController ac = new ActorController(); 10 11 Actor actor; 12 actor = ac.construct(ab); 13 14 System.out.println(actor.getType() + "的外观:"); 15 System.out.println("性别:" + actor.getSex()); 16 System.out.println("面容:" + actor.getFace()); 17 System.out.println("服装:" + actor.getCostume()); 18 System.out.println("发型:" + actor.getHairstyle()); 19 } 20 21 }
4. 结果及分析
当需要增加新的具体角色建造者时只需将新增具体角色建造者作为抽象角色建造者的子类,然后修改配置文件,原有的代码无需修改,完全符合开闭原则
指挥者类的深入讨论
指挥者类Director是建造者模式的重要组成部分,简单的Director类用于指导具体建造者如何构建产品,它按一定次序调用Builder的buildPartX()方法,控制调用的先后次序,并向客户端返回一个完整的产品对象。下面讨论几种Director的变化形式。
1. 省略Director
在有些情况下,为了简化系统结构,可以将Director和抽象建造者Builder进行合并,在Builder中提供逐步构建复杂产品对象的Construct()方法。由于Builder类通常为抽象类,因可以将construct()方法定义为静态(static)方法,以便客户端客户端直接调用。如果将游戏角色实例中的指挥者类ActorController省略,ActorBuilder类的代码修改如下:
1 public abstract class ActorBuilder { 2 protected static Actor actor = new Actor(); 3 4 public abstract void buildType(); 5 public abstract void buildSex(); 6 public abstract void buildFace(); 7 public abstract void buildCostume(); 8 public abstract void buildHairstyle(); 9 10 public static Actor construct(ActorBuilder ab){ 11 ab.buildType(); 12 ab.buildSex(); 13 ab.buildFace(); 14 ab.buildCostume(); 15 ab.buildHairstyle(); 16 return actor; 17 } 18 }
此时对应的客户端代码也将发生修改,代码如下:
1 public class Client { 2 3 public static void main(String[] args) { 4 ActorBuilder ab; 5 ab = (ActorBuilder)XMLUtil.getBean(); 6 7 Actor actor; 8 actor = ActorBuilder.construct(ab); 9 10 System.out.println(actor.getType() + "的外观:"); 11 System.out.println("性别:" + actor.getSex()); 12 System.out.println("面容:" + actor.getFace()); 13 System.out.println("服装:" + actor.getCostume()); 14 System.out.println("发型:" + actor.getHairstyle()); 15 } 16 }
还有一种更为简单的处理方法,可以将construct()方法中的参数去掉,直接在construct()方法中调用buildPartX()方法,代码如下:
1 public abstract class ActorBuilder { 2 protected static Actor actor = new Actor(); 3 4 public abstract void buildType(); 5 public abstract void buildSex(); 6 public abstract void buildFace(); 7 public abstract void buildCostume(); 8 public abstract void buildHairstyle(); 9 10 public Actor construct(){ 11 this.buildType(); 12 this.buildSex(); 13 this.buildFace(); 14 this.buildCostume(); 15 this.buildHairstyle(); 16 return actor; 17 } 18 }
客户端代码片段如下:
1 ... 2 ActorBuilder ab; 3 ab = (ActorBuilder)XMLUtil.getBean(); 4 5 Actor actor; 6 actor = ab.construct(); 7 ...
此时,construct()方法定义了buildPartX()方法的调用次序,为buildPartX()方法的执行提供了一个新的流程模板。以上两种对Director类的省略方式都不影响系统的灵活性和可扩展性,同时还简化了系统结构,但是加重了抽象建造者的职责。如果construct()方法较为复杂,待构建产品的组成部分较多,建议还是将construct()方法单独封装在Director中,这样更符合单一职责原则
2. 钩子方法的引入
建造者模式除了可以逐步构建一个复杂产品对象外,还可以通过Director类更加精确的控制产品的创建过程,例如增加一类成为钩子方法(Hook Method)的特殊方法来控制是否对某个buildPartX()进行调用。钩子方法的返回类型通常为boolean类型,方法名一般为isXXX(),钩子方法定义在抽象建造者中。例如可以在游戏角色的抽象建造者类ActorBuilder中定义一个方法isBareheaded(),用于判断某个角色是否为“光头(Bareheaded)”,在ActorBuilder为之提供一个默认实现,其返回值为false,代码如下:
1 public abstract class ActorBuilder { 2 protected Actor actor = new Actor(); 3 4 public abstract void buildType(); 5 public abstract void buildSex(); 6 public abstract void buildFace(); 7 public abstract void buildCostume(); 8 public abstract void buildHairstyle(); 9 10 //钩子方法 11 public boolean isBareHeaded(){ 12 return false; 13 } 14 15 public Actor createActor() { 16 return actor; 17 } 18 }
如果某个角色无需构建头发部件,例如“恶魔(Devil)”,则对应的具体建造者DevilBuilder将isBareheaded()方法覆盖,并将返回值改为true,代码如下:
1 public class DevilBuilder extends ActorBuilder { 2 @Override 3 public void buildType() { 4 actor.setType("恶魔"); 5 } 6 7 @Override 8 public void buildSex() { 9 actor.setSex("妖"); 10 } 11 12 @Override 13 public void buildFace() { 14 actor.setFace("丑陋"); 15 } 16 17 @Override 18 public void buildCostume() { 19 actor.setCostume("黑衣"); 20 } 21 22 @Override 23 public void buildHairstyle() { 24 actor.setHairstyle("光头"); 25 } 26 27 //覆盖钩子方法 28 @Override 29 public boolean isBareHeaded() { 30 return true; 31 } 32 33 }
同时,指挥者类ActorController的代码修改如下:
1 public class ActorController { 2 public Actor construct(ActorBuilder ab){ 3 Actor actor; 4 ab.buildType(); 5 ab.buildSex(); 6 ab.buildFace(); 7 ab.buildCostume(); 8 if(!ab.isBareHeaded()){ 9 ab.buildHairstyle(); 10 } 11 actor = ab.createActor(); 12 return actor; 13 } 14 }
当在客户端代码中指定具体建造者类型并通过指挥者来实现产品的逐步构建,将调用钩子方法isBareheaded()来判断游戏角色是否有头发,如果isBareheaded()方法返回true,即没有头发,将跳过构建发型的方法buildHairstyle()方法。通过引入钩子方法,可以在Director中对复杂产品的构建进行精细的控制,不仅指定buildPartX()方法的执行顺序,还可以控制是否需要执行某个buildPartX()方法
建造者模式优/缺点与使用环境
1. 建造者模式优点
(1)在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
(2)每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象,扩展方便,符合开闭原则。
(3)可以更加精确地控制产品的创建过程
2. 建造者模式缺点
(1)建造者模式所创建的产品一般具有较多的共同点,如果产品之间差异很大,则不适合用建造者模式,因此其适用范围受到一定限制
(2)如果产品内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,难以理解和维护
3. 建造者模式适用环境
(1)需要生产的产品对象有复杂的内部结构,这些产品对象通常包含多个成员变量
(2)需要生成的产品对象的属性相互依赖,需要指定其生产顺序
(3)对象的创建过程独立于创建该对象的类
(4)隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品