0. 前言 为了更好的构建软件,最近我也踏上了学习设计模式之路。学习来源主要是 韩老师的视频 ,但是韩老师的工厂模式和构造者模式讲得极差,于是又看了蜗牛学院的课程 ,才对构建者模式有了较为深刻的理解。
私以为学习设计模式,绝不是知道一个案例,“背下”针对这个案例的解决方案和代码。设计模式更重要的是思想,从代码的历史角度去了解为何要使用这样的模式,才是正确的学习方式。
好了,废话了很多,现在开始讲解到底什么是建造者设计模式。
1. 问题导入 需求:假设现有一客户需要购买电脑,电脑有高配、中配和低配之分。完成客户购买高配电脑的过程代码。
型号
cpu
内存
硬盘
高配
i9-9900X
16G
512G固态
中配
i7-8700
16G
256G固态
低配
i3-8100
8G
128G固态
嗯,问题非常的简单,先看看一种最简单的方法。
1.1 方式一 我们直接定义一个Computer类,然后Client类直接new一个Computer即可。
Computer类,我们假设一个Computer最要由cpu,memory和hardDisk来决定性能。
先看类图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Computer { private String cpu; private String memory; private String hardDisk; public Computer () { } public Computer (String cpu, String memory, String hardDisk) { this .cpu = cpu; this .memory = memory; this .hardDisk = hardDisk; } @Override public String toString () { return "Computer{" + "cpu='" + cpu + '\'' + ", memory='" + memory + '\'' + ", hardDisk='" + hardDisk + '\'' + '}' ; } public String getCpu () { return cpu; } public void setCpu (String cpu) { this .cpu = cpu; } public String getMemory () { return memory; } public void setMemory (String memory) { this .memory = memory; } public String getHardDisk () { return hardDisk; } public void setHardDisk (String hardDisk) { this .hardDisk = hardDisk; } }
再来看看Client是如何购购入的:
1 2 3 4 5 6 7 8 public class Client { public static void main (String[] args) { Computer computer = new Computer(); computer.setCpu("i9-9900X" ); computer.setMemory("16G" ); computer.setHardDisk("512G固态" ); } }
我想这也是大多数人的第一想法,因为这样的代码简单高效,但是缺点也非常的明显,我们每个client都需要自己手动地去set一个computer的所有属性,假设我们有成千上万的Client,那岂不是这段代码都要被写上成千上万次?
ok,相信很多人也能想到一种优化方式,“我把三种类型的(高、中、低配)的电脑封装好不就行了吗”?那我们再来看看吧:
1.2 方式二 友情提示:下面的方法还不是建造者模式。
同样的,先看类图:
正如上面所分析的那样,我们写三个封装类就好了呀。如下:
1 2 3 4 5 6 7 8 9 10 public class HighComputerBuilder { private Computer computer; public Computer build () { computer = new Computer(); computer.setCpu("i9-9900X" ); computer.setMemory("16G" ); computer.setHardDisk("512G固态" ); return computer; } }
1 2 3 4 5 6 7 8 9 10 public class MediumComputerBuilder { private Computer computer; public Computer build () { computer = new Computer(); computer.setCpu("i7-8700" ); computer.setMemory("16G" ); computer.setHardDisk("256G固态" ); return computer; } }
1 2 3 4 5 6 7 8 9 10 public class LowComputerBuilder { private Computer computer; public Computer build () { computer = new Computer(); computer.setCpu("i3-8100" ); computer.setMemory("8G" ); computer.setHardDisk("128G" ); return computer; } }
我们将三种类型的Computer的建造都封装到类,这样客户在使用的时候并不需要了解高中低三种配置的具体组装过程。只用使用相应的Builder就能得到Computer了。如下:
1 2 3 4 5 6 7 public class Client { public static void main (String[] args) { HighComputerBuilder hc = new HighComputerBuilder(); Computer computer = hc.build(); System.out.println(computer); } }
这样我们就可以解决方式一中,如果出现多个client,我们需要多次写set方法,毕竟我们将三种类型的电脑的build过程给封装起来了。再次提醒,虽然我这里的封装类中带有Builder字样,但是这里还不是建造者模式。
1.2.1 方式二-改 方式二其实还有个小问题,那就是Client依赖了具体的封装类,如果你稍微学过一点设计模式,那你肯定听说过依赖倒转原则 ,Client应该尽量的去依赖高层抽象,而不是具体细节。听起来也许有点复杂,但是给个类图,我相信你马上就懂了。
我们再定义一个抽象类(接口也行)ComputerBuilder,让所有具体的Builder去继承它。这样有什么好处呢?当然是实现Client和具体Builder之间的解耦咯。看看下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public abstract class ComputerBuilder { protected Computer computer; public abstract Computer build () ; } public class HighComputerBuilder extends ComputerBuilder { @Override public Computer build () { computer = new Computer(); computer.setCpu("i9-9900X" ); computer.setMemory("16G" ); computer.setHardDisk("512G固态" ); return computer; } } public class Client { public Computer buyComputer (ComputerBuilder cb) { return cb.build(); } public static void main (String[] args) { Client client = new Client(); Computer computer = client.buyComputer(new HighComputerBuilder()); System.out.println(computer); } }
可以看到buyComputer的参数是ComputerBuilder这样的抽象类,这样方便我们对ComputerBuilder的具体类进行扩展,比如我现在加了一个中高配的电脑Builder,我完全不需要修改buyComputer方法(这样也遵守了ocp原则),“什么,你的main方法不还是要传入具体的ComputerBuilder类吗?”,可是main方法中的内容可以写在任何其他地方啊?假设main不在client中呢?client是不是就完全和具体的ComputerBuilder解耦了?
1.2.2 方式二的优缺点 优点在前文也提到了,解决了方式一的需要重复写多个set方法。
现在主要谈谈缺点:我们仔细考虑一下在方式二中的三个具体Builder类,发现它们无一例外都是在build中new一个Computer,然后对属性进行设置。这样出现以下问题:
我们仍然在写着重复的代码,每多一个具体类,我们都需要new,然后再set。
每个具体类中的set过程可能不一,有可能某个具体类少set了一个cpu属性(可能是程序员在写时给忘记了),这样的语法是没问题的,编译器完全不会理会这样的“逻辑漏洞”,但这根本不符合一台computer的结构啊。
那怎么改进?也许你能想到,再进行一次抽象呗,我们定义好一个接口(或者抽象类),接口中放置制作一台电脑所必须的工序(setCpu, setMemory, setHardDisk)。然后各个具体Builder去实现这个接口(或抽象类)时必须重写这些方法。
1.3 方式三 友情提示,这里仍然不是构建者模式。
同样,我们先看类图:
现在来写代码,特别注意这里的build方法和前文的build方法的不同点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public abstract class ComputerBuilder { protected Computer computer = new Computer(); public abstract void setCpu () ; public abstract void setMemory ( ) ; public abstract void setHardDisk () ; public Computer build () { return this .computer; } } public class HighComputerBuilder extends ComputerBuilder { @Override public void setCpu () { computer.setCpu("i9-9900X" ); } @Override public void setMemory () { computer.setMemory("16G" ); } @Override public void setHardDisk () { computer.setHardDisk("512G固态" ); } } public class MediumComputerBuilder extends ComputerBuilder { @Override public void setCpu () { computer.setCpu("i7-8700" ); } @Override public void setMemory () { computer.setMemory("16G" ); } @Override public void setHardDisk () { computer.setHardDisk("256G固态" ); } } public class LowComputerBuilder extends ComputerBuilder { @Override public void setCpu () { computer.setCpu("i3-8100" ); } @Override public void setMemory () { computer.setMemory("8G" ); } @Override public void setHardDisk () { computer.setHardDisk("128G" ); } }
好,现在我们来看看如何在Client中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Client { public static void main (String[] args) { ComputerBuilder cb1 = new HighComputerBuilder(); cb1.setCpu(); cb1.setMemory(); cb1.setHardDisk(); Computer computer1 = cb1.build(); System.out.println(computer1); ComputerBuilder cb2 = new MediumComputerBuilder(); cb2.setCpu(); cb2.setMemory(); cb2.setHardDisk(); Computer computer2 = cb2.build(); System.out.println(computer2); } }
这TM不就变个样式回到方式一了吗?看了这么久你在玩我???
nonono,骚年,的确,这样看我们和方式一差别不大,如果存在多个client,仍然需要多次set,而且还构建得如此复杂。不过我们只要稍作一点改进,就可以来到真正的构造者模式啦。
1.4 方式四-主角入场-构造者模式 回忆一下,我们为什么要使用方式三?因为我们需要强制安排制作一台computer的工序(setCpu,setXX的那几个函数),但是我们有了这些工序,却把组装的过程(调用setCpu等方法)交给了Client。我们现在要做的就是再来一个类,它只负责“组装”好我们的computer,然后交给客户就行了。好,我们把这个类称为Director,指挥者,指挥如何组装一台computer。
同样,我们先看类图:
ok,来看看Director类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Director { private ComputerBuilder cb; public Director (ComputerBuilder cb) { this .cb = cb; } public void setCb (ComputerBuilder cb) { this .cb = cb; } public Computer build () { cb.setCpu(); cb.setMemory(); cb.setHardDisk(); return cb.build(); } }
client呢?
1 2 3 4 5 6 7 8 public class Client { public static void main (String[] args) { ComputerBuilder cb = new HighComputerBuilder(); Director director = new Director(cb); Computer computer = director.build(); System.out.println(computer); } }
yeah~~,我们再也不需要多次调用set方法了。来看一下这样做有哪些优点:
无需再client中手动调用多次set方法,减少重复代码量。
规定了制作一台Computer必须的工序(ComputerBuilder中的几个抽象方法),每个具体Builder类,必须实现这几个方法,没有任何具体Builder类能够“偷工减料”,保证最后的Computer一定会有Cpu,Memory和HardDisk。
将组装流程封装成一个单独的类,确定组成顺序。让组装过程和Client解耦。
由于ComputerBuilder这一抽象层,我们可以很轻松的扩展一个具体Builder类,比如我现在要生产一个中高配电脑,我直接建立一个类继承自ComputerBuilder,实现几个方法后就可以丢给Client用了,Client对我是如何制作这个电脑的一概不知。
这,就是建造着模式。我们贴一下它的wiki定义。
生成器模式 (英:Builder Pattern)是一种设计模式 ,又名:建造模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
2. 抽象工厂和建造者模式的区别 抽象工厂旨在创一系列的相关产品,产品是立即返回的,不设置相关的属性。
建造者模式旨在一步一步按照工序制作一个复杂的对象,需要设置相关属性后再返回。