文章目录
创建型模式
接下来我们来看看设计原则的最佳实践
设计模式的类型
根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 中所提到的,总共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。当然,我们还会讨论另一类设计模式:J2EE 设计模式。
序号 | 模式 & 描述 | 包括 |
---|---|---|
1 | 创建型模式 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。 | 工厂模式(Factory Pattern)抽象工厂模式(Abstract Factory Pattern)单例模式(Singleton Pattern)建造者模式(Builder Pattern)原型模式(Prototype Pattern) |
2 | 结构型模式 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。 | 适配器模式(Adapter Pattern)桥接模式(Bridge Pattern)过滤器模式(Filter、Criteria Pattern)组合模式(Composite Pattern)装饰器模式(Decorator Pattern)外观模式(Facade Pattern)享元模式(Flyweight Pattern)代理模式(Proxy Pattern) |
3 | 行为型模式 这些设计模式特别关注对象之间的通信。 | 责任链模式(Chain of Responsibility Pattern)命令模式(Command Pattern)解释器模式(Interpreter Pattern)迭代器模式(Iterator Pattern)中介者模式(Mediator Pattern)备忘录模式(Memento Pattern)观察者模式(Observer Pattern)状态模式(State Pattern)空对象模式(Null Object Pattern)策略模式(Strategy Pattern)模板模式(Template Pattern)访问者模式(Visitor Pattern) |
4 | J2EE 模式 这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。 | MVC 模式(MVC Pattern)业务代表模式(Business Delegate Pattern)组合实体模式(Composite Entity Pattern)数据访问对象模式(Data Access Object Pattern)前端控制器模式(Front Controller Pattern)拦截过滤器模式(Intercepting Filter Pattern)服务定位器模式(Service Locator Pattern)传输对象模式(Transfer Object Pattern) |
2.1 工厂设计模式
工厂设计模式属于创建型模式,创建型模式的核心是隐藏细节,并创建实例,我们看工厂模式中的模式都是能够向外提供类的实例对象的
其中工厂设计模式分为三种:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
在讲工厂设计模式之前,我们先来看看一个合理的运用设计模式设计出来的软件架构应该是怎么样的,这也是面向接口编程的设计方式
我们在面向接口编程中,模块和模块之间不能直接调用具体实现类,而是调用模块提供的接口,我们仔细想想在平时的编码中,有直接service层调用dao层的xxxDaoImpl
的吗?显然没有,如果直接调用了也差不多该换下一份工作了,一般都是注入接口,让容器注入一个实现类
工厂模式的核心也是隐藏内部实现,对外暴露接口实现具体逻辑,我们来看看工厂模式中的几个重要概念:
- 产品:就是具体的产品,例如下面代码中的
Hamburger
- 抽象产品:产品的抽象,例如
Food
- 产品簇:这个概念在抽象工厂中在场景中再解释
- 产品等级:同上
我们来看一个简单的例子,来理解上面的几个概念,另外在学习设计模式的时候一定要清醒的认识到,为什么要有设计模式,为了应对变化,以不变应万变,这里还有两个概念,就是我们习惯于把功能提供者称为作者
,把我们这些API调用工厂师叫做用户
// 作者做的抽象产品
interface Food{
void eat();
}
// 作者做的具体产品
class Hamburger implements Food{
@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}
// 用户的业务逻辑
public class AppTest {
public static void main(String[] args) {
Food food = new hamburger();
food.eat();
}
}
本来没有什么问题的,但是我一开始就提过,我们要应对这个不断变化的世界,现在变化来了,由于某些原因,作者把原来提供的类名改了,例如把hamburger
改为hamburger2
,这时候怎么办,是不是我们也要跟着改!!!
跟我说,什么叫耦合?一个改了另一个也要改,这就叫耦合;什么叫解耦?一个改了另一个不需要改,这就叫解耦!
有人说怎么可能,作者好好地没事会去该类名,闲得慌??你别说,笔者在项目中还经常遇到一些作者瞎改类名又不提示用户的,hutool
包是我们常用的工具包,但是有一些类名在包升级的时候直接修改,而且完全没有做兼容处理,有一次团队项目打包失败了,最后定位问题发现,这个作者之前类名单词拼写错误了
,然后升级版本的时候直接改了类名,导致我们升级版本导入一个不存在的类,导致项目出现问题,记得当时我们都说这个作者是个坑爹玩意,一看就是设计模式没学好
读者可以看看这个作者违反了什么原则?违反了开闭原则,正确的做法应该是给错误的类加过时的标记,重新写一个
我们来总结一下:
- 这样的设计非常的脆弱,为什么呢?只要作者修改了具体产品的类名,那么客户端就要一起修改,这样的代码就是耦合的
- 我们希望的效果是,无论服务端客户端如何修改,客户端代码都应该
无感知
,而不用去进行修改
那么我们如何进行改进呢?针对服务端代码一旦修改,客户端代码也要修改的问题
,我们直接使用简单工厂设计模式
2.1.1 简单工厂模式
设计模式的类型
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。它的本质就是通过传入不同的参数达到多态的目的
优点:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:
- 用户需要去记忆具体产品和常量之间的映射关系,例如:
FoodNumberEnum.HAMBURGER -> hamburger
- 如果具体产品非常多,在简单工厂里面映射关系会非常多
- 最重要的是,当用户需要拓展新产品时,就需要在作者的工厂源码中添加映射关系,违反
开闭原则
首先我们要弄清楚一个问题,简单工厂设计模式
是谁进行具体编码的?是我们(用户)去写吗?还是作者去写,其实是应该由作者去写
假设我们现在是这个作者,我们发现之前的设计确实不太合理,现在要进行优化,我们就可以这样写:
// 作者做的抽象产品
interface Food {
void eat();
}
// 作者做的具体产品
class hamburger implements Food {
@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}
class hamburger2 implements Food {
@Override
public void eat() {
System.out.println("这是作者修改后的汉堡包...");
}
}
class FoodFactory {
public enum FoodNumberEnum {
HAMBURGER(1001, "汉堡包"),
HAMBURGER2(1002, "修改后的汉堡包"),
;
private final Integer foodNumber;
private final String describe;
FoodNumberEnum(Integer foodNumber, String describe) {
this.foodNumber = foodNumber;
this.describe = describe;
}
public Integer getFoodNumber() {
return foodNumber;
}
public String getDescribe() {
return describe;
}
public static FoodNumberEnum getByType(int type) {
for (FoodNumberEnum constants : values()) {
if (constants.getFoodNumber() == type) {
return constants;
}
}
return null;
}
}
public static Food getFood(FoodNumberEnum foodNumberEnum) {
Food food = null;
switch (FoodNumberEnum.getByType(foodNumberEnum.getFoodNumber())) {
case HAMBURGER :
food = new hamburger();
break;
case HAMBURGER2 :
food = new hamburger2();
break;
default:
break;
}
return food;
}
}
// 用户的业务逻辑
public class AppTest {
public static void main(String[] args) {
Food food = FoodFactory.getFood(FoodFactory.FoodNumberEnum.HAMBURGER);
food.eat(); // 输出吃汉堡包...
}
}
我们来看这样做的好处是什么:
- 之前是我们直接创建对象,依赖于作者的代码;现在是依赖于工厂,由工厂创建对象
- 如果作者对原来的实现类做出了修改,也必须修改工厂里面的代码,注意,这里是
作者进行修改
,而不是用户修改,这样就做到了依赖倒置
,完成了解耦
,这样用户代码不用做出任何修改
- 之前由于作者修改代码导致用户也要修改其实还违背了
迪米特法则
,因为我们被迫去了解了作者的实现,其实我们是不关心如何实现的,我们只需要一个接口实现我们想要的功能即可!!!
可能有又有杠精要问了,要是作者把枚举也改了怎么办?这不是还是要改客户端代吗,我我我???直接好家伙
请杠精看看Spring中的工厂模式是怎么做的,后面笔者也会分析源码,我们在Spring中,不是一直写这样的代码吗???
@Component
public class XXX {
@Autowired(required = false)
private XXXBean xxxBean;
}
请问,这样做不管实现类怎么修改,只要注入IOC容器,我难道不能直接注入接口中吗?这就叫解耦,面向接口编程
我们总结一下简单工厂的优点:
- 把具体产品的类名,从客户端代码中解构出来了,服务端如果修改了服务端类名,客户端也不知道
- 这便符合了面向接口编程的思想,这里注意,这里的接口并不特指
interface
,而是指下层给上层暴露的东西,可以是一个方法、一个接口、甚至是一个工厂,并且这些接口必须稳定
,绝对不能改变
**那么缺点呢?**好像也没啥缺点感觉,又解耦了,又隐藏细节了,这里又不得不提一直提到的变
字了,学习设计模式,我们要将变
字贯穿整个学习过程
- 客户端不得不死记硬背那些枚举类和产品之间的映射关系,比如
FoodNumberEnum.HAMBURGER -> hamburger
- 如果有成千上万个产品,那么
简单工厂就会变得十分的臃肿
,造成映射关系爆炸 - 最重要的是如果变化来了,如果客户端需要拓展产品,首先我们不能改源代码(违反开闭原则),我们只能搞一个实现类实现自己的逻辑,但是工厂中又没有映射关系让我们创建这个实例,我们又得去修改工厂的源码,又违背了开闭原则,同时最重要的是,你这是去改别人提供的jar包呀我的天,你觉得你能看到被人的源代码吗?你能修改吗?????显然不能,那怎么解决呢?这就引出第二个工厂模式:
工厂方法设计模式
- 有的同学可能会觉得自己拓展的类,自己new一个不就好了,还改什么源代码?讲的有道理,但是如果你也是作者呢?你写的拓展是要给别人使用的呢?难道让读者去new一个你的实现类,这不违背
迪米特法则
了吗?用户只想要一个具体的实现,你现在要让用户去找你的实现类,这合理吗?这不合理,我们直接看工厂方法设计模式
是怎么解决的
最后来画一下类图,我们学设计模式,一定要能熟练画出类图
总结:
2.1.2 工厂方法模式
我们回顾一下上面简单工厂产生的问题,就是简单工厂只能提供作者提供好的产品,我们无法对产品进行拓展
工厂方法
定义一个用于创建产品的接口,由子类决定生产什么产品
- 坏处: 加一个类,需要加一个工厂,类的个数成倍增加,增加维护成本,增加系统抽象性和理解难度
- 好处: 符合开闭原则
总结:简单工厂模式 + 开闭原则 = 工厂方法
我们来改造一下上面有问题的代码,我们现在不将工厂写死,而是面向抽象编程,将工厂定义为一个接口,在作者的代码中提供一些基本的实现,例如创建Hamburger
、RichNoodle
的实现
// 作者做的抽象产品
interface Food {
void eat();
}
// 作者做的具体产品
class Hamburger implements Food {
@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}
class RichNoodle implements Food {
@Override
public void eat() {
System.out.println("过桥米线");
}
}
/** 定义工厂的接口 **/
interface FoodFactory {
Food getFood();
}
class HamburgerFactory implements FoodFactory {
@Override
public Food getFood() {
return new Hamburger();
}
}
class RichNoodleFactory implements FoodFactory {
@Override
public Food getFood() {
return new RichNoodle();
}
}
public class App {
public static void main(String[] args) {
// 拿到产生产品的工厂
FoodFactory foodFactory = new HamburgerFactory();
// 创建对应的产品
Food food = foodFactory.getFood();
food.eat();
}
}
我们会发现我们将工厂作为接口暴露之后就有一个好处,如果我们想要新增加一个产品,我们不需要去修改原来的产品,而是通过继承Food创建产品类,再通过暴露的工厂接口
创建工厂,再来实例化我们需要的产品,具体实现为:
/** 新的产品 **/
class PorkFeetRice implements Food{
@Override
public void eat() {
System.out.println("吃猪角饭...");
}
}
/** 生产猪角饭的工厂 **/
class PorkFeetRiceFactory implements FoodFactory{
@Override
public Food getFood() {
return new PorkFeetRice();
}
}
public class App {
public static void main(String[] args) {
// 拿到产生产品的工厂
FoodFactory foodFactory = new PorkFeetRiceFactory();
Food food = foodFactory.getFood();
food.eat();
}
}
我们可以看到,我们通过提供的工厂接口,并没有修改之前的工厂逻辑,又进行了拓展,并且新的产品和工厂实现类都是我们自己创建的,符合开闭原则
我们来总结一下工厂方法模式
的优点:
- 仍然具有简单工厂的优点,服务端修改了生产产品的逻辑时,用户端无感知
- 因为产品和工厂都是拓展出来的,所以不需要修改原来的代码,只需要创建一个新的产品和工厂即可
但是我们也有发现这样好像怪怪的,感觉就是暴露了个接口而已,读者可能会有以下的疑问:
- 虽然说好像不管是简单工厂也好,工厂方法也罢,虽然都做到了和具体实现解耦,用户不用关注实现是否发生了改变,但是,反观我们现在的代码,好像还在依赖于具体的工厂,说的就是上面的
PorkFeetRiceFactory
,如果我们每生产一种产品,都要去知道这种产品对应的工厂,这又违反了迪米特法则
,并且如果如果作者如果把工厂名字写错了,又会出现上面的问题 - 感觉折腾了一圈,又回到了原点,之前是依赖于具体的实现,现在是依赖具体的工厂,还是耦合的关系
我们来解释一下上面的两个问题:
- 首先,既然作者已经对外暴露了接口,那么作者有义务保存接口的稳定,不能出现改接口名的行为(或弃用)
- 其次工厂模式还可以隐藏一个实例创建的过程,学过spring等框架的同学就会知道,
在框架中一个实例的创建并不只是简简单单new那么简单
,可能还会牵涉到容器生命周期以及一些解析bean的操作,显然会复杂很多,所以工厂模式还帮我们隐藏了细节、封装了代码
,至于具体要使用那个工厂,是我们应该去了解的,毕竟每个工厂提供产品都会不同 - 很多问题现在简单的业务下并不是问题,但是请不要忘记
变
这个字,我们的代码为什么要分一层又一层,就是为了能够在之后改变后的业务场景依旧能够使用,如果读者还有问题,请复习复习Spring IOC容器的设计,如果没有工厂,IOC容器如果做到解析那么多注解,如何完成依赖注入,如果完成各种Spring预留的拓展接口,如果完成Bean的声明周期
总结一下缺点:
-
每一个层级的产品的产品都需要对应一个产品,不仅增加了编码的负担,还可能产生
类爆炸
的现象
我们现在来画一下工厂方法模式
的UML类图:
最后总结一下:
2.1.3 抽象工厂
抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象
优点:解决了工厂方法模式中类爆炸的问题,同时拥有其可拓展性的优点
缺点:产品族扩展非常困难,要
增加或删除
一个系列的某一产品,既要在抽象的工厂里加代码,又要在具体的里面产品加代码,违背开闭原则
我们来后顾一下前面的两种工厂模式的缺点
- 简单工厂:耦合与具体的工厂实现,没有良好的拓展性
- 工厂方法:会产生类爆炸的问题,每一个产品类都需要对应的工厂,代码臃肿
我们现在要通过工厂方法设计量类产品的生产,分别是食物Food
、饮料Drink
,其中食物有三种,饮料有两种,我们会发现食物需要定义一个接口,三个具体产品类,一个抽象工厂类,三个具体工厂类;饮料需要定义一个接口,两个具体产品类,一个抽象工厂类,两个具体工厂类,一共14个类
,我就想要产生五种类,好家伙,按照工厂方法的写法,直接写出14个类了
/****************** 抽象产品 *********************/
// 食物抽象类
interface Food {
void eat();
}
// 饮料抽象类
interface Drink{
void drink();
}
/****************** 具体产品 *********************/
// 食物的具体产品
class Hamburger implements Food {
@Override
public void eat() {
System.out.println("吃汉堡包...");
}
}
class RichNoodle implements Food {
@Override
public void eat() {
System.out.println("过桥米线");
}
}
class Cola implements Drink{
@Override
public void drink() {
System.out.println("喝可口可乐...");
}
}
class IcePeak implements Drink{
@Override
public void drink() {
System.out.println("喝冰峰...");
}
}
/****************** 抽象工厂类 *********************/
interface FoodFactory {
Food getFood();
}
interface DrinkFactory{
Drink getDrink();
}
/****************** 实例工厂类 *********************/
class HamburgerFactory implements FoodFactory {
@Override
public Food getFood() {
return new Hamburger();
}
}
class RichNoodleFactory implements FoodFactory {
@Override
public Food getFood() {
return new RichNoodle();
}
}
class ColaFactory implements DrinkFactory{
@Override
public Drink getDrink() {
return new Cola();
}
}
class IcePeakFactory implements DrinkFactory{
@Override
public Drink getDrink() {
return new IcePeak();
}
}
/*************** 新拓展的产品和工厂 **********************/
class PorkFeetRice implements Food{
@Override
public void eat() {
System.out.println("吃猪角饭...");
}
}
/** 生产猪角饭的工厂 **/
class PorkFeetRiceFactory implements FoodFactory{
@Override
public Food getFood() {
return new PorkFeetRice();
}
}
public class AbstractApp {
public static void main(String[] args) {
// 拿到产生产品的工厂
FoodFactory foodFactory = new PorkFeetRiceFactory();
Food food = foodFactory.getFood();
food.eat();
}
}
上面的代码就是工厂设计模式,其实工厂设计模式到抽象工厂只有一步之遥
,我们会发现上面导致类爆炸的原因就在于太多工厂类的接口了
,那我们就将工厂类的接口再进行抽象,例如食物和饮料的工厂接口统一进行抽象,再将各自的实例工厂也进行合并
/****************** 抽象工厂类 *********************/
interface AbstractFactory {
Food getFood();
Drink getDrink();
}
/****************** 实例工厂类 *********************/
class KFCFactory implements AbstractFactory {
@Override
public Food getFood() {
return new Hamburger();
}
@Override
public Drink getDrink() {
return new Cola();
}
}
显然这样通过抽象 + 组合
的方式,这样就可以减少一些类的产生
这样做的优点是就是在拥有工厂方法模式的优点下可以有效减少类的产生
缺点是好像在一个工厂类绑定死了具体的产品,例如KFCFactory -> Hamburger + Cola
,我们为什么在这个工厂生成这两种产品
现在我们补充一下上面没有提到的两个概念:
-
产品簇:多个有内在联系或有逻辑关系的产品,例如上面的KFC套餐就固定为
Hamburger + Cola
,这里就组成一个产品簇
-
产品等级:其实要弄清楚产品的等级我们可以看下面的图,
产品等级就是指由一个接口或者父类泛化的子类
,例如各种饮料、各种电冰箱,而产品簇
就是指由一个工厂生产的产品,看下图中美的生产的产品,这些就组成一个产品簇
我们可以看到如果要生产上面的25个具体产品,一共需要多少个抽象产品,多少个抽象工厂?都是五个;如果是工厂方法则需要25个类,可以看到通过不断组合的方式,可以大大减少抽象产品类和抽象工厂类的创建
👀👀👀 现在让我们想一想这样的设计有什么问题没有?我们好好想一想
-
新增产品簇:产品如果现在新增了一个工厂
京东
,那么我们只需要写一个京东的抽象工厂类,在写五个产品类分别继承上图中的抽象产品类,再加到自己的工厂类中,符合开闭原则 -
新增产品等级:如果我们需要新加一个手机的
产品等级
呢?想想我们该如何添加,首先写一个抽象产品类,然后写??等等,好像发现写不下去了叭,怎么好像又需要在其他的工厂里面添加自己的产品类??又违反了开闭原则
🎯总结抽象工厂的缺点:
- 当产品等级发生改变时(增加、减少),都会引起之前工厂代码的修改,违背开闭原则
所以设计模式有优点的同时也一定都有自己的局限性,我们要看场景来使用具体的工厂模式,笔者在下一节总结中进行分析
抽象工厂总结:
2.1.4 工厂模式总结
工厂方法 | 优点 | 缺点 |
---|---|---|
简单工厂 | 隐藏了类创建的细节,只需要类的标识就能创建一个类(多态) | 产生很多映射关系;当添加产品时违反开闭原则 |
工厂方法 | 符合开闭原则(简单工厂模式 + 开闭原则 = 工厂方法) | 加一个类,需要加一个工厂,类的个数成倍增加,增加维护成本,增加系统抽象性和理解难度 |
抽象工厂 | 解决了工厂方法模式中类爆炸的问题,同时拥有其可拓展性的优点 | 产品等级扩展非常困难,会违反开闭原则 |
工厂方法 | 适用场景 |
---|---|
简单工厂 | 产品不扩充,简单工厂最好 |
工厂方法 | 产品经常要拓展(产品簇与产品等级) |
抽象工厂 | 当产品等级固定,且产品簇需要拓展时 |
看了这么多好像这些工厂模式都不是我们心中最想要的结果,我们要的是既想马儿跑,又不想马儿吃草
,即又不想多写代码,又想拥有良好的拓展性,那么这样的方法有吗?当然有。Spring就帮我们解决了,想想Spring是如何帮我们解决这个问题的,这将在下一节中揭晓谜底
2.1.5 Spring中的工厂模式
上面分析了这么久,还是不能满足我们好吃懒做既想马儿跑,又不想马儿吃草
的需求,我们的目标是,既不能写过多的代码,就像工厂方法中类爆炸一样,又想要极致的拓展性,不能像抽象工厂一样不能添加产品等级
接下来让我们看看Spring中是如何解决的叭,可能很多同学平时框架用的爽,却根本不知道框架导致帮我们解决了什么问题,也不知道怎么解决的,这就导致一看源码就表示看不懂,很多面试题看了很多遍又会忘记,其实归根结底还是不理解,不会用,文章一开始我就讲过,最厉害的武功应该是忘记招式,无招胜有招!
直接揭晓答案叭,既想马儿跑,又不想马儿吃草
的方案是:动态工厂 + 反射
当然并不是Spring中没有用其他的工厂,当然也用到了,像静态工厂(简单工厂)也运用到了,并且更多的不是一种设计模式使用,而是多种,例如:策略设计模式 + 工厂模式 + 模板方法模式
首先从
BeanFactory
这个顶层工厂接口说起,BeanFactory
定义的是IOC容器最基本的规范可以说
BeanFactory
是 Spring 的心脏
。它就是 Spring IoC 容器的真面目。Spring 使用 BeanFactory 来实例化、配置和管理 Bean。
相信被面试过BeanFactory面试题的同学对下面的张图一定很熟悉
从上面我们可以看到BeanFactory
有三个实现接口,分别是ListableBeanFactory
、HierarchicalBeanFactory
和AutowireCapableBeanFactory
,并且井井有序,继承结构设计的非常优秀,其中:
-
BeanFactory作为一个主接口不继承任何接口,暂且称为一级接口
-
有3个子接口继承了它,进行功能上的增强。这3个子接口称为二级接口
-
ConfigurableBeanFactory可以被称为三级接口,对二级接口HierarchicalBeanFactory进行了再次增强,它还继承了另一个外来的接口SingletonBeanRegistry
-
ConfigurableListableBeanFactory
是一个更强大的接口,继承了上述的所有接口,无所不包,称为四级接口(这4级接口是BeanFactory的基本接口体系。继续,下面是继承关系的2个抽象类和2个实现类:)
-
AbstractBeanFactory作为一个抽象类,实现了三级接口ConfigurableBeanFactory大部分功能
-
AbstractAutowireCapableBeanFactory同样是抽象类,继承自AbstractBeanFactory,并额外实现了二级接口AutowireCapableBeanFactory
-
DefaultListableBeanFactory继承自AbstractAutowireCapableBeanFactory,实现了最强大的四级接口ConfigurableListableBeanFactory,并实现了一个外来接口BeanDefinitionRegistry,它并非抽象类
-
最后是最强大的XmlBeanFactory,继承自DefaultListableBeanFactory,重写了一些功能,使自己更强大,但是这个类现在以及被标记为过期类,Spring官方建议使用:BeanFactory懒加载 或者
ApplicationContext
中的逻辑来替换它
看看我们上面写的是什么玩意,跟玩具一样,就抽象了一层,不仅想解耦还想高拓展,实际的开发场景往往是十分复杂的,学习设计模式一定要把变
字牢牢的记在心里。Spring中复杂的继承和抽象结构,就是为了满足尽可能多的应用场景
名字 | 作用 |
---|---|
(一级接口)BeanFactory | 定义的是IOC容器最基本的规范,核心方法getBean() |
(二级接口)ListableBeanFactory | 实现对Bean实例的枚举,以及对默些公共特征Bean的管理(同一产品等级) |
(二级接口)HierarchicalBeanFactory | 在BeanFactory定义的功能上增加了对父容器的定义,表示Bean继承关系 |
(二级接口)AutowireCapableBeanFactory | Bean的创建注入并提供对Bean的初始化前后拓展性处理 |
(三级接口)ConfigurableBeanFactory | 提供配置Factory的各种方式 |
(四级接口)ConfigurableListableBeanFactory | 修改Bean定义信息和分析Bean的功能,实现了预实例化单例Bean以及冻结当前工厂配置的功能 |
(抽象类)AbstractBeanFactory | Bean的创建和信息描述抽象方法,由继承者实现 |
(抽象类)AbstractAutowireCapableBeanFactory | 实现Bean的创建并解决依赖注入问题,实现createBean()方法 |
(实现类)DefaultListableBeanFactory | 对Bean容器完全成熟的默认实现,可对外使用 |
我们先来看(一级接口)BeanFactory,可以看到BeanFactory的核心方法就一个getBean()
,还有一些获取Bean属性的方法
接下来就是(二级接口)HierarchicalBeanFactory,它的方法更少,核心就是getParentBeanFactory()
,我们知道不同的Bean有不同的工厂加载,这个方法就是获取这个Bean工厂的父工厂的,主要是为了解决IOC容器循环依赖的问题,其中在IOC容器中定义一个Bean是否一样,不仅要判断Bean是否一样,还需要判断创建其的工厂是否一样,这里就不展开讲了
(二级接口)ListableBeanFactory
(二级接口)AutowireCapableBeanFactory:
(三级接口)ConfigurableBeanFactory主要是提供配置Factory的各种方法,主要的方法有:
- setConversionService():设置转换器
- addBeanPostProcess():添加Bean的后置处理器
- destoryBean():销毁Bean
(四级接口)ConfigurableListableBeanFactory:修改Bean定义信息和分析Bean的功能,实现了预实例化单例Bean以及冻结当前工厂配置的功能
(抽象类)AbstractBeanFactory,Bean的创建和信息描述抽象方法,定义createBean()方法,核心方法有:
- getBeanDefinition():BeanDefinition我们熟,Bean的定义,这个方法就是获取Bean的信息
- createBean():创建Bean,由继承者来实现
(抽象类)AbstractAutowireCapableBeanFactory:实现Bean的创建并解决依赖注入问题,实现createBean()方法
好重点来了兄弟们,看来这么多层的抽象,终于到了创建Bean的地方,让我们到源码里面看看Spring是如何优雅的
创建产品
的
看来这么多,可能读者还是没明白在Spring中是如果动态创建产品的,其实过程很简单,我们想想在平时的编码中会干什么
首先我们会将项目分层,分为controller、service、dao等层次,我们使用SpringBoot的时候只需要加一个注解SpringBootApplication
,就能自动扫描当前包及其子包下面被标记了@Component
注解的类,然后加载到Spring的容器中,我们想想,我们需要管实现类的类名是什么吗?需要管实现内的代码改没改吗?都不需要,Spring会帮我们将其注册到IOC容器中,并且依靠强大的DI来进行注入,每次启动项目都会动态的去扫描,并且依靠各种工厂去创建产品
@Component
public class XXXService {
@Autowired(required = false)
private XXXDao xxxdao;
}
我们来思考到底比之前的工厂模式好在哪里
-
首先,用户添加
@Component
后,这个Bean的定义就被注册到IOC容器了,相当于简单工程中做映射;Bean工厂在创建这个实例时不需要知道具体类型,因为是靠反射创建的实例,无论什么类型都可以创建,这样就消除了工厂类对接口实现类的依赖
,当我们想要拓展产品时,只需要写实现类并将其交给IOC容器即可
总结:总而言之,言而总之,Spring是如何解决工厂模式问题的?
- 通过
动态工厂 + 反射
,通过添加@Component
注解动态获取Bean的定义,解决简单工厂中不能动态添加产品的问题 - 通过反射解决抽象工厂中无法拓展产品等级的问题;并且解决工厂方法中类爆炸的问题
- 通过强大的依赖注入,解决接口与实现类之间耦合的问题,并且可以进行自动装配,条件装配,真正面向接口编程
2.1.6 工作中工厂方法使用
没有实际业务场景,一切都是无源之水、无本之木
,是空洞的,现在我们举一个笔者实际开发中配合IOC容器使用的工厂模式的例子
现在有一个场景,就是需要做登录,但是我们不确定现在移动端的同学做几端,可能会有手机号登录、账号密码登录、qq登录、微信登录、PC端登录、网页端登录。这些登录的具体实现肯定是不一样的,而且我们并不知道到底要做几种策略,并且之后一定会有所拓展
所以我们一般会用IOC容器 + 工厂模式 + 策略模式 + 模板方法模式
来完成这些功能
首先我们要定义一个策略接口,用来动态获取Bean(产品)的定义,该接口继承InitializingBean
,我们后面动态添加的策略只需要实现该接口,通过回调我们的注册方法,就能将自己添加到我们自己的工厂中
/**
* 策略接口
*
* @author Eureka
* @since 2022/9/25 11:59
*/
public interface LoginHandle extends InitializingBean {
/**
* 具体的登录逻辑
*/
void login(Map<String,String> params);
}
设计我们自己的登录工厂
public class LoginFactory {
private static Map<String, LoginHandle> loginStrategyFactoryMap = new ConcurrentHashMap<>();
public static LoginHandle getLoginStrategy(String loginSign) {
return loginStrategyFactoryMap.get(loginSign);
}
public static void register(String loginSign, LoginHandle loginHandle) {
if (StringUtils.isEmpty(loginSign) || Objects.isNull(loginHandle)) {
throw new RuntimeException("登录策略注册失败,参数错误");
}
// 将策略注册到工厂中
loginStrategyFactoryMap.put(loginSign, loginHandle);
}
}
例如我现在是手机号登录,我们就写一个具体的实现类,并实现我们的策略接口
/**
* 使用电话号码登录具体策略
*
* @author Eureka
* @since 2022/10/4 12:02
*/
@Slf4j
@Component
public class PhoneNumberLoginStrategy implements LoginHandle {
private static final String PHONE_NUMBER_LOGIN_STRATEGY = "PHONE_NUMBER_LOGIN_STRATEGY";
@Override
public void login(Map<String, String> params) {
// 放具体的登录策略
if (log.isDebugEnabled()) {
log.debug("用户通过手机号码登录,参数为:{}", params);
}
}
@Override
public void afterPropertiesSet() throws Exception {
// 将自己注册到我们自己定义的工厂中
LoginFactory.register(PHONE_NUMBER_LOGIN_STRATEGY, this);
}
}
我们来思考一下上面的策略模式 + 工厂模式解决了之前工厂模式的什么问题
- 首先,新增策略(产品),只需要实现
LoginHandle
策略接口,通过将自己的映射关系注册到工厂中,解决了简单工厂中无法拓展的问题 - 其次,每个策略上用
@Component
标记,表示这个Bean的实例交给Spring完成,通过反射解决抽象工厂无法拓展产品等级的问题
我们现在想要调用不同的执行策略,只需要让前端小哥哥在用户登录的时候传不同的标识,例如LoginSign = PHONE_NUMBER_LOGIN_STRATEGY
,我们只需要
@Controller
public class LoginController {
@PostMapping("/user/login")
public ResponseEntity<Void> login(@RequestBody Map<String, String> params) {
// 调用对应的登录逻辑
LoginHandle loginHandle = LoginFactory.getLoginStrategy(params.get("LoginSign"));
loginHandle.login(params);
return ResponseEntity.ok().build();
}
}
这样如果下次需要新增加一种登录,我只需要写好具体的策略并实现策略接口,再和前端小哥沟通好标识就行了,上面这个Controller
中的代码完全不用修改
,这样既然解耦了,也能做到极致的拓展,并且完成不用写多余的代码,真正做到又想马儿跑,又不给马儿吃草
但是这样就ok了吗???笔者一再强调,开发讲究的就是一个变字,并且细心的小伙伴可能已经发现现在的类图好像看起来还是有点不太合理,现在的类图长这样
好像我们现在的实现类现在耦合接口LoginHandle
了,有的同学会有疑问,什么叫耦合接口???不就应该面向接口编程吗?难道接口会变?对,接口就是会变,如果现在我们辛辛苦苦写了十种策略,马上要下班了,现在前端小哥说,还有一个下线的功能别忘记写了
好,现在BBQ了,我们得去该接口LoginHandle
,新添加logout
的接口,并且在十种策略里面都添加下线的实现,违反了开闭原则
,并且我们假设,有的策略是不需要登出功能,而且又导致有些策略类不得不实现一个空方法,有的同学会问,难道不能在重新写一套工程和策略吗?当然不好,注册登录登出,本来就应该在一个体系里面
总结一下上面的问题:
- 依赖于
LoginHandle
接口,并且实现类必须要实现里面的所有接口,如果实现类有不想实现的,也必须实现
这里我们再引入板方法模式
,也就是在接口与实现类之前再套一层,看多了源码的同学肯定知道,那有接口下面直接就是实现的,一般都要再套一层抽象层进行解耦
我们现在再来捋一捋思路,首先我们现在接口里面新增加一个方法
public interface LoginHandle extends InitializingBean {
/**
* Bean实例化后回调该方法,将自己注册到自定义的工厂中
*/
void login(Map<String, String> params);
/**
* 登出功能
*/
void logout();
}
我们再创建一层抽象层AbstractLoginHandle
,称为模板方法
/**
* 模板方法
*
* @author Eureka
* @since 2022/10/4 12:52
*/
public class AbstractLoginHandle implements LoginHandle {
@Override
public void login(Map<String, String> params) {
throw new UnsupportedOperationException();
}
@Override
public void logout() {
throw new UnsupportedOperationException();
}
/**
* 其中这个方法是必须要实现的,声明为抽象
*/
@Override
abstract public void afterPropertiesSet();
}
接下来修改我们具体的登录实现类,可以发现中间套了一层到好处是不用实现接口里面所有的方法,只需要有需要的实现即可,例如现在微信登录不需要登出功能,我们就可以这样写
/**
* 使用登录登录具体策略
*
* @author Eureka
* @since 2022/10/4 12:02
*/
@Slf4j
@Component
public class WXrLoginStrategy extends AbstractLoginHandle {
private static final String WX_LOGIN_STRATEGY = "WX_LOGIN_STRATEGY";
@Override
public void login(Map<String, String> params) {
// 放具体的登录策略
if (log.isDebugEnabled()) {
log.debug("用户微信登录,参数为:{}", params);
}
}
@Override
public void afterPropertiesSet() throws Exception {
// 将自己注册到我们自己定义的工厂中
LoginFactory.register(WX_LOGIN_STRATEGY, this);
}
}
我们开看一下现在的类图:
现在的这个结构就算比较合理的了,但是还可以拓展,这里就不展开了
最后再提一嘴,我们这里的工厂模式是不是不属于上面提到的任何一种设计模式,其实这上面的模板方法模式和严格意义上的也有区别,希望读者始终记住,请不要死板的套用设计模式,网上随便搞篇博客上来就是写各种接口抽象类,你会发现其实用起来不是特别的顺手,只有多总结多归纳多思考,当我们真正理解后,就不会局限于那种设计模式了,而是下意识就会去这样设计,一看类图就知道这里不合理,做到无招胜有招
2.2 原型模式
原型模式(Prototype Pattern)
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用
现在我们来想一个场景,例如我们现在要写周报,大家作为互联网打工人肯定每周都是要向老板反馈工作内容的,现在我们发现周报里面需要填很多信息,例如姓名、部门、职位等等,这些信息基本都不会变,但是每次填周报我们又需要填一遍,这样显然不合理,我们其实想的是,写周报只需要填汇报内容即可,如下图所示:
那我们应该怎么办呢?是不是希望能够保存一下上一次编辑的模板,下次直接修改内容
就可以了,我们用一个类来表示一下
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport{
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private LocalDateTime submitDate;
}
public class AppTest {
public static void main(String[] args) {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("奈李")
.setSummary("本周主要完成了七大设计原则和工厂模式的学习")
.setPlain("在下周的工作中完成原型模式学习")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
// 简单输出一下
System.out.println(weekReport);
// 第二周周报
WeekReport weekReport2 = new WeekReport();
weekReport2.setEmp("奈李")
.setSummary("本周主要完成了剩下设计模式的学习")
.setPlain("在下周会完成阿里巴巴开发手册学习")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
System.out.println(weekReport2);
}
}
我们会发现其实下一周要进行汇报时,我们只需要修改总结和下周计划的内容,但是现在我们却只能再新建一个对象重复上述代码
我们想要什么,想要的是直接克隆
出一个对象来,将需要改变的填一下,不变的用之前的就好
我们现在通过克隆
来实现上面的需求,在Java中只需要实现Cloneable
,并重写克隆方法即可
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport implements Cloneable {
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private LocalDateTime submitDate;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class AppTest {
public static void main(String[] args) throws CloneNotSupportedException {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("奈李")
.setSummary("本周主要完成了七大设计原则和工厂模式的学习")
.setPlain("在下周的工作中完成原型模式学习")
.setDepartment("互联网事业部")
.setSubmitDate(LocalDateTime.now());
// 简单输出一下
System.out.println(weekReport);
// 第二周周报 直接copy第一周的周报改改进行
WeekReport weekReport2 = (WeekReport) weekReport.clone();
weekReport2.setSummary("本周主要完成了剩下设计模式的学习")
.setPlain("在下周会完成阿里巴巴开发手册学习")
.setSubmitDate(LocalDateTime.now());
System.out.println(weekReport2);
}
}
这就是原型设计模式,是不是感觉挺简单的,这里还有几个点要注意一下
- 上面的克隆其实只是
浅拷贝
- 使用clone方法创建对象,并不会调用该对象的构造器
- 因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
2.2.1 深拷贝浅拷贝
类型 | 特点 |
---|---|
浅拷贝 | Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝 |
深拷贝 | 会将原本对象中的数组、容器对象、引用对象等另行拷贝(完全是新的对象) |
有的同学在这里总是傻傻分不清楚,就知道浅拷贝就是引用类型不拷贝,然后指向之前对象的堆空间,分不清成员变量中的引用是啥
上面的可以把setSubmitDate
看做一个指针,你弄了一个新对象给它,当然会在堆空间里面新申请一块内存,当然就不一样了
那我们如何深拷贝呢?其实也很简单,既然原来的Object.clone()
方法无法克隆引用类型,那我们自己来克隆就好了
其中大部分类其实都是实现了clone方法的
@Override
protected Object clone() throws CloneNotSupportedException {
WeekReport cloneWeekReport = (WeekReport) super.clone();
// Date类型需要自己手动进行克隆
Date cloneSubmitDate = (Date) cloneWeekReport.getSubmitDate().clone();
cloneWeekReport.setSubmitDate(cloneSubmitDate);
return cloneWeekReport;
}
但是这样显然很麻烦,如果我们克隆的是自己定义的对象,或者是对象里面套对象,这样层层嵌套的形式,显然就有点麻烦了
2.2.2 封装深拷贝工具
所以我们一般是用序列化和反序列化来做的,并且会封装成一个工具,这个拷贝在开发中还是非常常见的,例如我们经常将一个PO转换为一个VO(就是拷贝对象),一般用Spring自带的BeanUtils
,但是它只能浅拷贝,我们现在自己封装一个,可以选择继承Spring自带的BeanUtils
,或者就放deepClone
这一个方法就行
/**
* 拷贝Bean
*/
@Slf4j
public final class BeanUtil extends BeanUtils {
private BeanUtil() {
throw new UnsupportedOperationException();
}
/**
* 拷贝属性
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) {
if (checkNull(source, targetClass)) {
return null;
}
try {
T newInstance = targetClass.newInstance();
copyProperties(source, newInstance);
return newInstance;
} catch (Exception e) {
log.error("error: ", e);
return null;
}
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T object) {
T cloneObject = null;
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.close();
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
cloneObject = (T) objectInputStream.readObject();
objectInputStream.close();
} catch (ClassNotFoundException | IOException e) {
log.info("拷贝异常::", e);
}
return cloneObject;
}
public static <T> T copyProperties(Object source, Class<T> targetClass, String... ignoreProperties) {
if (checkNull(source, targetClass)) {
return null;
}
try {
T newInstance = targetClass.newInstance();
copyProperties(source, newInstance, ignoreProperties);
return newInstance;
} catch (Exception e) {
log.error("error: ", e);
return null;
}
}
/**
* 拷贝集合
*/
public static <T> List<T> copyProperties(List<?> sources, Class<T> targetClass) {
if (checkNull(sources, targetClass) || sources.isEmpty()) {
return new ArrayList<>();
}
return sources.stream().map(source -> copyProperties(source, targetClass)).collect(Collectors.toList());
}
private static <T> boolean checkNull(Object source, Class<T> targetClass) {
return Objects.isNull(source) || Objects.isNull(targetClass);
}
}
现在在来看看原来的拷贝,这里需要注意进行序列化的对象需要实现Serializable
接口
@Data // 自动生成getter setter 和 toString方法
@Accessors(chain = true) // 开启链式编程
class WeekReport implements Serializable {
private static final long serialVersionUID = 4455534412412L;
private int id;
private String emp;
private String summary;
private String plain;
private String suggestion;
private String department;
private Date submitDate;
}
public class DeepCloneTest {
public static void main(String[] args) throws CloneNotSupportedException, InterruptedException {
WeekReport weekReport = new WeekReport();
weekReport.setEmp("奈李")
.setSummary("本周主要完成了七大设计原则和工厂模式的学习")
.setPlain("在下周的工作中完成原型模式学习")
.setDepartment("互联网事业部")
.setSubmitDate(new Date());
// 简单输出一下
System.out.println("weekReport的时间:" + weekReport.getSubmitDate());
// 第二周周报 直接copy第一周的周报改改进行
WeekReport weekReport2 = BeanUtil.deepClone(weekReport);
weekReport2.setSummary("本周主要完成了剩下设计模式的学习")
.setPlain("在下周会完成阿里巴巴开发手册学习");
// 这样才是修改同一个引用指向堆空间里面的值
weekReport2.getSubmitDate().setTime(0);
System.out.println(weekReport == weekReport2);
System.out.println("weekReport2的时间:" + weekReport2.getSubmitDate());
System.out.println("weekReport的时间:" + weekReport.getSubmitDate());
}
}
2.2.3 原型模式小结
是不是感觉其实原型模式也没啥,确实也没啥,所以23中设计模式其实有的模式其实还是挺简单的
2.3 建造者模式
建造者模式(Builder Pattern)
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。
- 优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
- 缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
例如我们现在有一个电脑类,里面有一些属性
@Data
@Accessors(chain = true)
class Compute {
private String cpu;
private String gpu;
private String memory;
private String hd;
}
public class BuilderTest {
public static void main(String[] args) {
Compute compute = new Compute()
.setCpu("i7-10700")
.setGpu("3080 Ti")
.setMemory("32G")
.setHd("1T");
System.out.println(compute);
}
}
这样其实也有些问题:
- 对象实例化的时候就必须要为每一个属性赋值,比较麻烦
- 对于使用者来说,相当于给你一堆零件,然后需要自己组装
现在来改进一下,这里作者需要专门创建一个ComputeBuilder
类,来专门负责组装电脑的过程
@Data
@Accessors(chain = true)
class Compute {
private String cpu;
private String gpu;
private String memory;
private String hd;
}
// 电脑构建者类,并且必须关联一个产品
class ComputeBuilder {
private Compute compute = new Compute();
// 构建方法
public Compute builder() {
return compute.setCpu("i7-10700")
.setGpu("3080 Ti")
.setMemory("32G")
.setHd("1T");
}
}
public class BuilderTest {
public static void main(String[] args) {
// 创建一个建造者
ComputeBuilder computeBuilder = new ComputeBuilder();
// 创建电脑
Compute compute = computeBuilder.builder();
System.out.println(compute);
}
}
上述代码的好处是:
- 由建造者隐藏了创建对象的复杂过程
但是这样的缺点是什么?
- 好像封装的太厉害了,无论客户需要什么,返回都是一样的配置
我们再改造一下
@Data
@Accessors(chain = true)
class Compute {
private String cpu;
private String gpu;
private String memory;
private String hd;
}
interface IcomputeBuilder{
Compute builder();
}
// 高级配置
class HighComputeBuilder implements IcomputeBuilder{
private Compute compute = new Compute();
// 构建方法
public Compute builder() {
return compute.setCpu("i7-10700")
.setGpu("3080 Ti")
.setMemory("32G")
.setHd("1T");
}
}
class MiddleComputeBuilder implements IcomputeBuilder{
private Compute compute = new Compute();
// 构建方法
public Compute builder() {
return compute.setCpu("i5-9500")
.setGpu("2080 Ti")
.setMemory("16G")
.setHd("1T");
}
}
class LowComputeBuilder implements IcomputeBuilder{
private Compute compute = new Compute();
// 构建方法
public Compute builder() {
return compute.setCpu("i3-8500")
.setGpu("1080 Ti")
.setMemory("8G")
.setHd("500G");
}
}
public class BuilderTest {
public static void main(String[] args) {
// 创建一个建造者
IcomputeBuilder highBuilder = new HighComputeBuilder();
IcomputeBuilder middleBuilder = new MiddleComputeBuilder();
IcomputeBuilder lowBuilder = new LowComputeBuilder();
// 创建最厉害的电脑
Compute highCompute = highBuilder.builder();
System.out.println(highCompute);
// 中等的电脑
Compute middleCompute = middleBuilder.builder();
System.out.println(middleCompute);
// 一般的电脑
Compute lowCompute = lowBuilder.builder();
System.out.println(lowCompute);
}
}
这样以来用户可以通过选取不同的建造者,来生产不同的产品,但是这里还是有问题
- 我们发现在不同的构造者中,有重复的代码,既然有重复的代码,那就有
坏味道
- 构建的过程是不稳定的,如果某个建造者遗漏掉了哪一步,那么生产出来的产品就是不合格的,但是编译器却不会报错
我们得再进行改造
@Data
@Accessors(chain = true)
class Compute {
private String cpu;
private String gpu;
private String memory;
private String hd;
}
interface IcomputeBuilder {
IcomputeBuilder cpu();
IcomputeBuilder gpu();
IcomputeBuilder memory();
IcomputeBuilder hd();
Compute builder();
}
// 高级配置
class HighComputeBuilder implements IcomputeBuilder {
private Compute compute = new Compute();
@Override
public IcomputeBuilder cpu() {
compute.setCpu("i7-10700");
return this;
}
@Override
public IcomputeBuilder gpu() {
compute.setGpu("3080 Ti");
return this;
}
@Override
public IcomputeBuilder memory() {
compute.setMemory("32G");
return this;
}
@Override
public IcomputeBuilder hd() {
compute.setHd("1T");
return this;
}
// 构建方法
public Compute builder() {
return compute;
}
}
public class BuilderTest {
public static void main(String[] args) {
// 创建一个建造者
IcomputeBuilder highBuilder = new HighComputeBuilder();
// 创建最厉害的电脑
Compute highCompute = highBuilder.cpu().gpu().memory().hd().builder();
System.out.println(highCompute);
}
}
我们看这样进行构建的优点:
- 建造者类中的建造过程是稳定的。不会漏掉某一步!!这样当客户端想扩展建造者时,也不会漏掉某一步
缺点:
- 如果有多个建造者,代码任然会有重复
- 现在又变成了客户端自己配置电脑,又违反了
迪米特法则
。(这相当于,你去电脑城配电脑,虽然不用你亲自组装电脑,但是你必须指挥
那个装机boy,下一步该干啥,下一步该干啥,虽然隐藏细节,但是还是要指挥
我们想要的是连指挥过程也要隐藏起来
@Data
@Accessors(chain = true)
class Computer {
private String cpu;
private String gpu;
private String memory;
private String hd;
}
interface IComputerBuilder {
IComputerBuilder cpu();
IComputerBuilder gpu();
IComputerBuilder memory();
IComputerBuilder hd();
Computer builder();
}
// 隐藏指挥命令的细节
class Director {
public Computer build(IComputerBuilder computerBuilder) {
// 指挥builder进行组装
return computerBuilder.cpu().gpu().memory().hd().builder();
}
}
// 高级配置
class HighComputerBuilder implements IComputerBuilder {
private Computer computer = new Computer();
@Override
public IComputerBuilder cpu() {
computer.setCpu("i7-10700");
return this;
}
@Override
public IComputerBuilder gpu() {
computer.setGpu("3080 Ti");
return this;
}
@Override
public IComputerBuilder memory() {
computer.setMemory("32G");
return this;
}
@Override
public IComputerBuilder hd() {
computer.setHd("1T");
return this;
}
// 构建方法
public Computer builder() {
return computer;
}
}
public class BuilderTest {
public static void main(String[] args) {
// 创建一个建造者
IComputerBuilder highBuilder = new HighComputerBuilder();
// 创建指挥者
Director director = new Director();
// 由指挥者进行指挥
Computer highComputer = director.build(highBuilder);
System.out.println(highComputer);
}
}
- 建造者负责建造,指挥者负责指挥,创建对象的过程是稳定的(IComputerBuilder接口负责稳定),创建对象的过程也不会有重复代码(指挥者完成)
- 当需要拓展新的产品时,不需要修改原来的代码,只需要实现构建者接口,然后交给指挥者完成即可,这里就将构造和流程分别由构造者和指挥者进行解耦
- 建造者作为中间层,进行解耦
最后画一下UML类图:
2.3.1 建造者与工程模式的区别
- 工厂模式只负责创建实例,并不关心里面的属性和构建的过程;建造者模式更加关注构建的过程,通过一些规范的流程、标准构建合格的产品