@[toc]
创建型模式
创建型模式提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性。
| 类型| 实现要点 |
|--|--|
| 工厂方法 | 定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。 |
| 抽象工厂| 提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。 |
|建造者 |将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示 |
| 原型 | ⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。 |
| 单例| 保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。 |
概述
建造者模式所完成的内容就是通过将多个简单对象通过⼀步步的组装构建出⼀个复杂对象的过程。
举个例子玩王者荣耀的时的初始化界⾯;有三条路、有树⽊、有野怪、有守卫塔等等,甚⾄依赖于你的⽹络情况会控制清晰度。⽽当你换⼀个场景进⾏其他不同模式的选择时,同样会建设道路、树⽊、野怪等等,但是他们的摆放和⼤⼩都有不同。这⾥就可以⽤到建造者模式来初始化游戏元素。
⽽这样的根据相同的物料 ,不同的组装所产⽣出的具体的内容,就是建造者模式的最终意图,也就是将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
Case
模拟装修公司对于设计出⼀些套餐装修服务的场景。
套餐服务:欧式豪华、轻奢⽥园、现代简约等等,⽽这些套餐的后⾯是不同的商品的组合。例如: ⼀级&⼆级吊顶、多乐⼠涂料、圣象地板、⻢可波罗地砖等等,按照不同的套餐的价格选取不同的品牌组合,最终再按照装修⾯积给出⼀个整体的报价。
这⾥我们就模拟装修公司想推出⼀些套餐装修服务,按照不同的价格设定品牌选择组合,以达到使⽤建造者模式的过程。
模拟工程
提供了装修中所需要的物料: ceilling(吊顶) 、 coat(涂料) 、 floor(地板) 、tile(地砖) ,这四项内容。
【 物料接⼝ 】
public interface Matter {
/**
* 场景;地板、地砖、涂料、吊顶
*/
String scene();
/**
* 品牌
*/
String brand();
/**
* 型号
*/
String model();
/**
* 平米报价
*/
BigDecimal price();
/**
* 描述
*/
String desc();
}
物料接⼝提供了基本的信息,以保证所有的装修材料都可以按照统⼀标准进⾏获取
具体的材料,我们以一级顶为例
/**
* 吊顶
* 品牌;装修公司自带
* 型号:一级顶
*/
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";
}
}
其他材料同样的实现了Matter接口 。
以上就是装修配置单 ,接下我们去使⽤不同的物料组合出不同的套餐服务。
Bad Impl
讲道理没有ifelse解决不了的逻辑,不⾏就在加⼀⾏!
我们先使⽤这样很直⽩的⽅式去把功能实现出来,在通过设计模式去优化完善。
⼀个类⼏千⾏的代码你是否⻅过,那今天⻅识⼀下有这样潜质的类!
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 块⾥,都包含着不通的材料(吊顶,⼆级顶、涂料,⽴邦、地砖,⻢可波罗),最终⽣成装修清单和装修成本。
- 最后提供获取装修详细信息的⽅法,返回给调⽤⽅,⽤于知道装修清单。
【单元测试】
接下来我们通过junit单元测试的⽅式验证接⼝服务,强调⽇常编写好单测可以更好的提⾼系统的健壮度。
@Test
public void test_DecorationPackageController(){
DecorationPackageController decoration = new DecorationPackageController();
// 豪华欧式
System.out.println(decoration.getMatterList(new BigDecimal("132.52"),1));
// 轻奢田园
System.out.println(decoration.getMatterList(new BigDecimal("98.25"),2));
// 现代简约
System.out.println(decoration.getMatterList(new BigDecimal("85.43"),3));
}
-------------------------------------------------------
装修清单
套餐等级:1
套餐价格:198064.39 元
房屋面积:132.52 平米
材料清单:
吊顶:装修公司自带、二级顶、平米价格:850 元。
涂料:多乐士(Dulux)、第二代、平米价格:719 元。
地板:圣象、一级、平米价格:318 元。
-------------------------------------------------------
装修清单
套餐等级:2
套餐价格:119865.00 元
房屋面积:98.25 平米
材料清单:
吊顶:装修公司自带、二级顶、平米价格:850 元。
涂料:立邦、默认级别、平米价格:650 元。
地砖:马可波罗(MARCO POLO)、缺省、平米价格:140 元。
-------------------------------------------------------
装修清单
套餐等级:3
套餐价格:90897.52 元
房屋面积:85.43 平米
材料清单:
吊顶:装修公司自带、一级顶、平米价格:260 元。
涂料:立邦、默认级别、平米价格:650 元。
地砖:东鹏瓷砖、10001、平米价格:102 元。
以上这段使⽤ ifelse ⽅式实现的代码,⽬前已经满⾜的我们的也许功能。
但随着业务的快速发展要求,会提供很多的套餐针对不同的户型。那么这段实现代码将迅速扩增到⼏千⾏,甚⾄在修修改改中,已经像膏药⼀样难以维护。
Better Impl (建造者模式重构代码)
接下来使⽤建造者模式来进⾏代码优化,也算是⼀次很⼩的重构
建造者模式主要解决的问题是在软件系统中,有时候⾯临着"⼀个复杂对象"的创建⼯作,其通常由各个部分的⼦对象⽤⼀定的过程构成;由于需求的变化,这个复杂对象的各个部分经常⾯临着重⼤的变化,但是将它们组合在⼀起的过程却相对稳定。
这⾥我们会把构建的过程交给 创建者 类,⽽创建者通过使⽤我们的 构建⼯具包 ,去构建出不同的 装修套餐。
【工程结构】
建造者模式代码类关系
建造者模型结构
核⼼类是建造者模式的具体实现。与 ifelse 实现⽅式相⽐,多出来了两个额外的类。具体功能如下:
- Builder ,建造者类具体的各种组装由此类实现
- DecorationPackageMenu ,是 IMenu 接⼝的实现类,主要是承载建造过程中的填充器。这是⼀套承载物料和创建者中间衔接的内容。
【装修包接口】
public interface IMenu {
/**
* 吊顶
*/
IMenu appendCeiling(Matter matter);
/**
* 涂料
*/
IMenu appendCoat(Matter matter);
/**
* 地板
*/
IMenu appendFloor(Matter matter);
/**
* 地砖
*/
IMenu appendTile(Matter matter);
/**
* 明细
*/
String getDetail();
}
接⼝类中定义了填充各项物料的⽅法; 吊顶 、 涂料 、 地板 、 地砖 ,以及最终提供获取全部明细的⽅法。
【装修包实现】
/**
* 装修包
*/
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 ,也就可以⾮常⽅便的⽤于连续填充各项物料。
- 同时在填充时也会根据物料计算平⽶数下的报价,吊顶和涂料按照平⽶数适量乘以常数计算。
- 最后同样提供了统⼀的获取装修清单的明细⽅法
【 建造者⽅法】
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()); // 地砖,东鹏
}
}
建造者的使⽤中就已经⾮常容易了,统⼀的建造⽅式,通过不同物料填充出不同的装修⻛格; 豪华欧式 、 轻奢⽥园 、 现代简约 ,如果将来业务扩展也可以将这部分内容配置到数据库⾃动⽣成。但整体的思想还可以使⽤创建者模式进⾏搭建。
【单元测试】
@Test
public void test_Builder(){
Builder builder = new Builder();
// 豪华欧式
System.out.println(builder.levelOne(132.52D).getDetail());
// 轻奢田园
System.out.println(builder.levelTwo(98.25D).getDetail());
// 现代简约
System.out.println(builder.levelThree(85.43D).getDetail());
}
-------------------------------------------------------
装修清单
套餐等级:豪华欧式
套餐价格:198064.39 元
房屋面积:132.52 平米
材料清单:
吊顶:装修公司自带、二级顶、平米价格:850 元。
涂料:多乐士(Dulux)、第二代、平米价格:719 元。
地板:圣象、一级、平米价格:318 元。
-------------------------------------------------------
装修清单
套餐等级:轻奢田园
套餐价格:119865.00 元
房屋面积:98.25 平米
材料清单:
吊顶:装修公司自带、二级顶、平米价格:850 元。
涂料:立邦、默认级别、平米价格:650 元。
地砖:马可波罗(MARCO POLO)、缺省、平米价格:140 元。
-------------------------------------------------------
装修清单
套餐等级:现代简约
套餐价格:90897.52 元
房屋面积:85.43 平米
材料清单:
吊顶:装修公司自带、一级顶、平米价格:260 元。
涂料:立邦、默认级别、平米价格:650 元。
地砖:东鹏瓷砖、10001、平米价格:102 元。
测试结果是⼀样的,调⽤⽅式也基本类似。但是⽬前的代码结构却可以很⽅便的很有调理的进⾏扩展业务开发。⽽不是以往⼀样把所有代码都写到 ifelse
⾥⾯。
小结
当⼀些基本物料不会变,⽽其组合经常变化的时候 ,就可以选择这样的设计模式来构建代码。
此设计模式满⾜了单⼀职责原则以及可复⽤的技术、建造者独⽴、易扩展、便于控制细节⻛险。
同时当出现特别多的物料以及很多的组合后,类的不断扩展也会造成难以维护的问题。但这种设计结构模型可以把重复的内容抽象到数据库中,按照需要配置。这样就可以减少代码中⼤量的重复。