一、定义
建造者模式的核心目的是通过使用多个简单对象一步步地构建出一个复杂对象,通过控制操作台,一步步地组装出坦克。
例如,《王者荣耀》游戏的初始化界面有道路、树木、野怪和守卫塔等。
换一个场景选择其他模式时,同样会建设道路、树木、野怪和守卫塔等,但是它们的摆放位置和大小各有不同。
这种初始化游戏元素的场景就可以使用建造者模式。
这种根据相同的物料、不同的组装方式产生出具体内容,就是建造者模式的最终意图,即将一个复杂的构建与其表示分离,用同样的构建过程可以创建不同的表示
二、问题背景
这里模拟房屋装修公司设计出一些不同风格样式的装修套餐场景,来体现建造者模式的使用方法
很多装修公司都会提供一些套餐服务,一般会有:豪华欧式、轻奢田园和现代简约装修服务套餐等。而这些套餐的背后是不同装修材料和设计风格的组合,例如一级顶、二级顶、多乐士涂料、立邦涂料、圣象地板、德尔地板、马可波罗地砖、东鹏地砖等。按照不同的套餐价格,选取不同的品牌进行组合,最终再结合装修面积给出整体报价。
下面模拟装修公司推出的一些装修服务套餐,按照不同的价格组合品牌,并介绍使用建造者模式实现这一需求
在模拟的装修材料工程中,提供了如下类
- ceilling(吊顶材料)包:LevelOneCeiling、LevelTwoCeiling;
- coat(涂料材料)包:DuluxCoat、LiBangCoat;
- floor(地板材料)包:DerFloor、ShengXiangFloor;
- tile(地砖材料)包:DongPengTile、MarcoPoloTile
装修材料接口提供了基本的方法获取信息,以保证所有不同规格和种类的装修材料都可以按照统一标准被获取。
/**
* 装修物料
*/
public interface Matter {
/**
* 场景;地板、地砖、涂料、吊顶
*/
String scene();
/**
* 品牌
*/
String brand();
/**
* 型号
*/
String model();
/**
* 平米报价
*/
BigDecimal price();
/**
* 描述
*/
String desc();
}
(1)吊顶材料
/**
* 吊顶
* 品牌;装修公司自带
* 型号:一级顶
*/
public class LevelOneCeiling implements Matter {
public String scene() {
return "吊顶";
}
public String brand() {
return "装修公司自带";
}
public String model() {
return "一级顶";
}
public BigDecimal price() {
return new BigDecimal(260);
}
public String desc() {
return "造型只做低一级,只有一个层次的吊顶,一般离顶120-150mm";
}
}
/**
* 吊顶
* 品牌;装修公司自带
* 型号:二级顶
*/
public class LevelTwoCeiling implements Matter {
public String scene() {
return "吊顶";
}
public String brand() {
return "装修公司自带";
}
public String model() {
return "二级顶";
}
public BigDecimal price() {
return new BigDecimal(850);
}
public String desc() {
return "两个层次的吊顶,二级吊顶高度一般就往下吊20cm,要是层高很高,也可增加每级的厚度";
}
}
(2)涂料材料(coat)
/**
* 涂料
* 品牌;多乐士(Dulux)
*/
public class DuluxCoat implements Matter {
public String scene() {
return "涂料";
}
public String brand() {
return "多乐士(Dulux)";
}
public String model() {
return "第二代";
}
public BigDecimal price() {
return new BigDecimal(719);
}
public String desc() {
return "多乐士是阿克苏诺贝尔旗下的著名建筑装饰油漆品牌,产品畅销于全球100个国家,每年全球有5000万户家庭使用多乐士油漆。";
}
}
/**
* 涂料
* 品牌;立邦
*/
public class LiBangCoat implements Matter {
public String scene() {
return "涂料";
}
public String brand() {
return "立邦";
}
public String model() {
return "默认级别";
}
public BigDecimal price() {
return new BigDecimal(650);
}
public String desc() {
return "立邦始终以开发绿色产品、注重高科技、高品质为目标,以技术力量不断推进科研和开发,满足消费者需求。";
}
}
(3)地板材料(floor)
/**
* 地板
* 品牌;德尔(Der)
*/
public class DerFloor implements Matter {
public String scene() {
return "地板";
}
public String brand() {
return "德尔(Der)";
}
public String model() {
return "A+";
}
public BigDecimal price() {
return new BigDecimal(119);
}
public String desc() {
return "DER德尔集团是全球领先的专业木地板制造商,北京2008年奥运会家装和公装地板供应商";
}
}
/**
* 地板
* 品牌:圣象
*/
public class ShengXiangFloor implements Matter {
public String scene() {
return "地板";
}
public String brand() {
return "圣象";
}
public String model() {
return "一级";
}
public BigDecimal price() {
return new BigDecimal(318);
}
public String desc() {
return "圣象地板是中国地板行业著名品牌。圣象地板拥有中国驰名商标、中国名牌、国家免检、中国环境标志认证等多项荣誉。";
}
}
(4)地砖材料(floor)
/**
* 地砖
* 品牌:东鹏瓷砖
*/
public class DongPengTile implements Matter {
public String scene() {
return "地砖";
}
public String brand() {
return "东鹏瓷砖";
}
public String model() {
return "10001";
}
public BigDecimal price() {
return new BigDecimal(102);
}
public String desc() {
return "东鹏瓷砖以品质铸就品牌,科技推动品牌,口碑传播品牌为宗旨,2014年品牌价值132.35亿元,位列建陶行业榜首。";
}
}
/**
* 地砖
* 品牌;马可波罗(MARCO POLO)
*/
public class MarcoPoloTile implements Matter {
public String scene() {
return "地砖";
}
public String brand() {
return "马可波罗(MARCO POLO)";
}
public String model() {
return "缺省";
}
public BigDecimal price() {
return new BigDecimal(140);
}
public String desc() {
return "“马可波罗”品牌诞生于1996年,作为国内最早品牌化的建陶品牌,以“文化陶瓷”占领市场,享有“仿古砖至尊”的美誉。";
}
}
以上是本次装修公司所提供的装修配置单,接下来会通过不同的物料组合出不同的服务套餐。
三、违背设计模式的设计实现
没有if…else解决不了的逻辑,不行就再加一行!
这里先使用不加设计的方式实现功能,之后再通过设计模式优化完善。
一般使用这种实现方式的代码都会集中在一个类中,里面包含大量的if…else逻辑。
既不具有复杂的代码结构,也不具有良好的扩展性。
如果应对非常简单的业务,还是可以使用的。
对于装修包的类DecorationPackageController,按照一个类里有多个if…else代码的方式实现。
public class DecorationPackageController {
public String getMatterList(BigDecimal area, Integer level) {
List<Matter> list = new ArrayList<Matter>(); // 装修清单
BigDecimal price = BigDecimal.ZERO; // 装修价格
// 豪华欧式
if (1 == level) {
LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊顶,二级顶
DuluxCoat duluxCoat = new DuluxCoat(); // 涂料,多乐士
ShengXiangFloor shengXiangFloor = new ShengXiangFloor(); // 地板,圣象
list.add(levelTwoCeiling);
list.add(duluxCoat);
list.add(shengXiangFloor);
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(duluxCoat.price()));
price = price.add(area.multiply(shengXiangFloor.price()));
}
// 轻奢田园
if (2 == level) {
LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊顶,二级顶
LiBangCoat liBangCoat = new LiBangCoat(); // 涂料,立邦
MarcoPoloTile marcoPoloTile = new MarcoPoloTile(); // 地砖,马可波罗
list.add(levelTwoCeiling);
list.add(liBangCoat);
list.add(marcoPoloTile);
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
price = price.add(area.multiply(marcoPoloTile.price()));
}
// 现代简约
if (3 == level) {
LevelOneCeiling levelOneCeiling = new LevelOneCeiling(); // 吊顶,二级顶
LiBangCoat liBangCoat = new LiBangCoat(); // 涂料,立邦
DongPengTile dongPengTile = new DongPengTile(); // 地砖,东鹏
list.add(levelOneCeiling);
list.add(liBangCoat);
list.add(dongPengTile);
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelOneCeiling.price()));
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
price = price.add(area.multiply(dongPengTile.price()));
}
StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
"装修清单" + "\r\n" +
"套餐等级:" + level + "\r\n" +
"套餐价格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
"房屋面积:" + area.doubleValue() + " 平米\r\n" +
"材料清单:\r\n");
for (Matter matter: list) {
detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米价格:").append(matter.price()).append(" 元。\n");
}
return detail.toString();
}
}
首先,这段代码要解决的问题是接收入参:房屋面积(area)、装修等级(level),根据不同类型的装修等级选择不同的材料。
其次,在实现过程中可以看到每一段if代码块中包含着不同的材料(吊顶为二级顶;涂料为立邦;地砖为马可波罗),最终生成装修清单和装修价格。
最后,提供获取装修详细信息的方法,返回给调用方,便于客户了解装修清单。
虽然以上这段使用if…else方式实现的代码可以满足些许功能,但随着公司业务的快速发展,会针对不同的户型提供更多的套餐。这段实现代码将迅速扩增到几千行,甚至不断地修改,最终难以维护
四、问题改进
在软件系统开发中,有时会面临一个复杂对象的创建工作,其通常由各个部分的子对象用一定过程构建出来,随着需求的迭代,这个复杂对象的各个部分经常面临重大的变化,但是将它们组合在一起的过程却相对稳定,这种场景就适合用建造者模式。
这里会把构建的过程交给创建者类,而创建者通过使用构建工具包构建出不同的装修套餐。
建造者模式代码类关系如图:
建造者模式代码工程有三个核心类,这三个核心类是建造者模式的具体实现。
与使用if…else判断方式实现逻辑相比,它额外新增了两个类,具体功能如下:
- Builder:建造者类具体的各种组装,都由此类实现。
- DecorationPackageMenu:是IMenu接口的实现类,主要承载建造过程中的填充器,相当于一套承载物料和创建者中间衔接的内容。
也可以从装修材料参考图的视角看待这类工程,更便于理解,如图所示
接下来分别介绍几个类的功能的具体实现方式。
(1)定义装修包接口
接口类定义了填充吊顶、涂料、地板、地砖各种材料的方法,以及最终提供获取全部明细的方法。
(2)实现装修包接口
/**
* 装修包
*/
public class DecorationPackageMenu implements IMenu {
private List<Matter> list = new ArrayList<Matter>(); // 装修清单
private BigDecimal price = BigDecimal.ZERO; // 装修价格
private BigDecimal area; // 面积
private String grade; // 装修等级;豪华欧式、轻奢田园、现代简约
private DecorationPackageMenu() {
}
public DecorationPackageMenu(Double area, String grade) {
this.area = new BigDecimal(area);
this.grade = grade;
}
public IMenu appendCeiling(Matter matter) {
list.add(matter);
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
return this;
}
public IMenu appendCoat(Matter matter) {
list.add(matter);
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
return this;
}
public IMenu appendFloor(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price()));
return this;
}
public IMenu appendTile(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price()));
return this;
}
public String getDetail() {
StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
"装修清单" + "\r\n" +
"套餐等级:" + grade + "\r\n" +
"套餐价格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
"房屋面积:" + area.doubleValue() + " 平米\r\n" +
"材料清单:\r\n");
for (Matter matter: list) {
detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米价格:").append(matter.price()).append(" 元。\n");
}
return detail.toString();
}
}
在装修包的实现中,每一种方法都返回了this对象本身,可以非常方便地用于连续填充各种物料。
同时,在填充时也会根据物料计算相应面积的报价,吊顶和涂料按照面积乘以单价计算。
最后,同样提供了统一的获取装修清单的明细方法。
(3)建造者类创建
public class Builder {
public IMenu levelOne(Double area) {
return new DecorationPackageMenu(area, "豪华欧式")
.appendCeiling(new LevelTwoCeiling()) // 吊顶,二级顶
.appendCoat(new DuluxCoat()) // 涂料,多乐士
.appendFloor(new ShengXiangFloor()); // 地板,圣象
}
public IMenu levelTwo(Double area){
return new DecorationPackageMenu(area, "轻奢田园")
.appendCeiling(new LevelTwoCeiling()) // 吊顶,二级顶
.appendCoat(new LiBangCoat()) // 涂料,立邦
.appendTile(new MarcoPoloTile()); // 地砖,马可波罗
}
public IMenu levelThree(Double area){
return new DecorationPackageMenu(area, "现代简约")
.appendCeiling(new LevelOneCeiling()) // 吊顶,二级顶
.appendCoat(new LiBangCoat()) // 涂料,立邦
.appendTile(new DongPengTile()); // 地砖,东鹏
}
}
最后,在建造者的使用中就已经非常容易了。统一的建造方式通过不同物料填充出不同的装修风格:豪华欧式、轻奢田园和现代简约。
如果公司扩展业务,也可以将这部分内容配置到数据库中自动生成,但整体过程仍然可以使用建造者模式的思想进行搭建。
五、总结
通过上面对建造者模式的使用,可以总结出选择该设计模式的条件:当一些基本材料不变,而其组合经常变化时
此设计模式满足了单一职责原则及可复用的技术,建造者独立、易扩展、便于控制细节风险。
出现特别多的物料及组合时,类的不断扩展也会造成难以维护的问题。
但这种设计模式可以把重复的内容抽象到数据库中,按照需要配置,减少大量的重复代码。
虽然设计模式能带给我们一些设计思想,但在平时的开发中如何清晰地提炼出符合此思路的建造模块是比较困难的。
需要经过一些练习,不断承接更多的项目来获得经验。
有时代码写得好,往往是通过复杂的业务、频繁的变化和不断的挑战,逐步积累而来的。