设计模式
一、设计模式基础
1.1 什么是设计模式
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
1995年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了23种设计模式,从此树立了软件设计模式领域的里程碑,人称【GoF设计模式】。
1.2 学习设计模式的意义
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式有以下优点:
- 可以提高程序员的思维能力、编程能力和设计能力。
- 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
- 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
1.3 设计模式的基本要素
- 模式名称
- 问题
- 解决方案
- 效果
1.4 GoF 23
GoF 23
- 一种思维,一种态度,一种进步
创建型模式(5种):描述怎样去创建一个对象
- 单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式
结构型模式(7种):描述如何将我们的类或者对象按照某种布局组成一些更大的结构
- 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
行为性模式(11种):描述类或者对象之间怎能够相互协作,去共同完成单个对象无法完成的任务,主要是分配一些职责
- 模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
1.5 OOP七大原则
- 开闭原则:对扩展开放,对修改关闭
- 里式替换原则:继承必须确保超类所拥有的的性质在子类中仍然成立
- 依赖倒置原则:要面向接口编程,不要面向实现编程
- 单一职责原则:控制类的粒度大小(粒度越粗,说明做的方法越多),将对象解耦、提高其内聚性
- 接口隔离原则:要为各个类建立它们需要的专用接口
- 迪米特法则:只与你的直接朋友交谈,不跟“陌生人”说话
- 合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现
二、创建型模式
2.1 单例模式
2.1.1 单例模式
单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
角色分析:Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())
单例模式的通用源代码如下所示:
package com.kmu.shu.singleton;
public class Singleton {
private static final Singleton singleton = new Singleton();
//限制产生多个对象
private Singleton(){
}
//通过该方法获得实例对象
public static Singleton getSingleton(){
return singleton;
}
//类中其他方法,尽量是static
public static void doSomething(){
}
}
(1)自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人坐在这个位置
只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。
(2)皇帝类实现
package com.kmu.shu.singleton;
//皇帝类
public class Emperor {
private static final Emperor emperor =new Emperor(); //初始化一个皇帝
private Emperor(){
//世俗和道德约束,目的就是不希望产生第二个皇帝
}
public static Emperor getInstance(){
return emperor;
}
//皇帝发话了
public void say(){
System.out.println("朕是唯一的一个皇帝....");
}
}
(3)臣子类实现
package com.kmu.shu.singleton;
//臣子类
public class Minister {
public static void main(String[] args) {
for(int day=0;day<3;day++){
Emperor emperor = Emperor.getInstance();
emperor.say();
}
}
}
(4)运行结果
2.1.2 单例模式小结
-
优点
(1) 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
(2) 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
(3)单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
(4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
-
缺点
(1)单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽 象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
(2)单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
(3) 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
-
使用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:
(1)要求生成唯一序列号的环境;
(2)在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
(3)创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
(4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
单例模式是23个模式中比较简单的模式,应用也非常广泛:
如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。
如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期
2.2 工厂模式
作用:
- 实现了创建者和调用者的分离
- 详细分类:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
OOP七大原则:
-
开闭原则:一个软件的实体应当对扩展开放,对修改关闭
-
依赖倒置原则:要针对接口编程,不要针对实现编程
-
迪米特法则:只与你的直接朋友通信,不要和陌生人说话
核心本质:
- 实例化对象不使用new,用工厂方法代替
- 将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦
三种模式:
- 简单工厂模式:用来生产同一等级结构中的任意产品(对于增加新的产品,需要求扩展已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)
- 抽象工厂模式:围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
2.2.1 简单工厂模式
传统方法:
-
车接口
package com.kmu.shu.factory.simple; public interface Car { void name(); }
-
车类
package com.kmu.shu.factory.simple; public class Tesla implements Car{ @Override public void name() { System.out.println("特斯拉"); } } public class WuLing implements Car{ @Override public void name() { System.out.println("五菱宏光"); } }
-
消费者
package com.kmu.shu.factory.simple; public class Consumer { public static void main(String[] args) { //1.传统方式:接口,所有的实现类!拿车需要new车! Car wuLing = new WuLing(); Car tesla = new Tesla(); wuLing.name(); tesla.name(); } }
简单工厂模式:
-
创建一个车工厂
package com.kmu.shu.factory.simple; //静态工厂模式(简单工厂模式) //弊端:增加一个新的产品,如果你不修改代码,做不到! //车工厂:直接拿车就可以了 //开闭原则:不改变原来的类!使用简单工厂需要改变 public class CarFactory { //方法一:判断是什么车返回什么车 public static Car getCar(String car){ if(car.equals("五菱")){ return new WuLing(); }else if(car.equals("特斯拉")){ return new Tesla(); }else { return null; } } //方法二:想要什么返回什么 public static Car getWuLing(){ return new WuLing(); } public static Car getTesla(){ return new Tesla(); } }
-
消费者
package com.kmu.shu.factory.simple; public class Consumer { public static void main(String[] args) { //1.传统方式:接口,所有的实现类! /*Car wuLing = new WuLing(); Car tesla = new Tesla(); wuLing.name(); tesla.name();*/ //2.使用工厂创建:Consumer,不需要new车,直接通过工厂去拿车 Car wuLing = CarFactory.getCar("五菱"); Car tesla = CarFactory.getCar("特斯拉"); wuLing.name(); tesla.name(); } }
-
现在需要增加一个大众
public class DaZhong implements Car{ @Override public void name() { System.out.println("大众"); } }
-
这时候就需要在原有的代码上修改车工厂类去生产一辆大众车(违反了开闭原则!)
package com.kmu.shu.factory.simple; //静态工厂模式(简单工厂模式) //弊端:增加一个新的产品,如果你不修改代码,做不到! //车工厂:直接拿车就可以了 //开闭原则:不改变原来的类!使用简单工厂需要改变 public class CarFactory { //方法一:判断是什么车返回什么车 public static Car getCar(String car){ if(car.equals("五菱")){ return new WuLing(); }else if(car.equals("特斯拉")){ return new Tesla(); }else if(car.equals("大众")){ return new DaZhong(); }else { return null; } } //方法二:想要什么返回什么 public static Car getWuLing(){ return new WuLing(); } public static Car getTesla(){ return new Tesla(); } public static Car getDaZhong(){ return new DaZhong(); } }
-
画图分析
2.2.2 工厂方法模式:解决简单工厂存在的开闭原则问题
-
车接口
package com.kmu.shu.factory.method; //车接口 public interface Car { void name(); }
-
车工厂接口
package com.kmu.shu.factory.method; //工厂方法模式: //车工厂接口 public interface CarFactory { Car getCar(); }
-
特斯拉车
package com.kmu.shu.factory.method; public class Tesla implements Car { @Override public void name() { System.out.println("特斯拉"); } }
-
特斯拉车工厂
package com.kmu.shu.factory.method; public class TeslaFactory implements CarFactory{ @Override public Car getCar() { return new Tesla(); } }
-
五菱宏光
package com.kmu.shu.factory.method; public class WuLing implements Car { @Override public void name() { System.out.println("五菱宏光"); } }
-
五菱宏光车工厂
package com.kmu.shu.factory.method; public class WuLingFactory implements CarFactory { @Override public Car getCar() { return new WuLing(); } }
-
消费者
package com.kmu.shu.factory.method; public class Consumer { public static void main(String[] args) { Car wuLing = new WuLingFactory().getCar(); Car tesla = new TeslaFactory().getCar(); wuLing.name(); tesla.name(); } }
-
增加一个大众车
package com.kmu.shu.factory.method; public class DaZhong implements Car{ @Override public void name() { System.out.println("大众"); } }
-
大众车工厂
package com.kmu.shu.factory.method; public class DaZhongFactory implements CarFactory{ @Override public Car getCar() { return new DaZhong(); } }
-
消费者
package com.kmu.shu.factory.method; public class Consumer { public static void main(String[] args) { Car wuLing = new WuLingFactory().getCar(); Car tesla = new TeslaFactory().getCar(); wuLing.name(); tesla.name(); //增加一个大众车 Car daZhong = new DaZhongFactory().getCar(); daZhong.name(); } }
-
画图分析(虽然满足了开闭原则,可以横向扩展,但是很明显发现类变多了!)
2.2.3 简单工厂和工厂方法比较
- 结构复杂度:简单工厂优于工厂方法
- 代码复杂度:简单工厂优于工厂方法
- 客户端(消费者类)编程复杂度:简单工厂优于工厂方法
- 管理复杂度:简单工厂优于工厂方法
根据设计原则:工厂方法模式!满足了开闭原则
根据实际业务:简单工厂模式!
2.2.4 抽象工厂模式(工厂的工厂)
**定义:**抽象工厂模式提供了一个创建一系列相关或者相互依赖的接口,无需指定它们具体的类
适用场景:
- 客户端(应用层)不依赖产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品族)一起实体创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体的实现
- 适用于产品比较稳定的情况,不需要增加新的产品
优点:
- 具体的产品在应用层的代码隔离,无需关心创建的细节
- 将一个系列的产品统一到一起创建
缺点:
- 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难
- 增加了系统的抽象性和理解难度
抽象工厂模式:
-
UML类图
-
产品族理解
-
IPhoneProduct接口
package com.kmu.shu.abstractFactory; //手机产品接口 public interface IPhoneProduct { //开机 void start(); //关机 void shutdown(); //打电话 void callup(); //发短信 void sendSMS(); }
-
HuaweiPhone
package com.kmu.shu.abstractFactory; //华为手机 public class HuaweiPhone implements IPhoneProduct { @Override public void start() { System.out.println("开启华为手机"); } @Override public void shutdown() { System.out.println("关闭华为手机"); } @Override public void callup() { System.out.println("华为手机打电话"); } @Override public void sendSMS() { System.out.println("华为手机发短信"); } }
-
XiaomiPhone
package com.kmu.shu.abstractFactory; //小米手机 public class XiaomiPhone implements IPhoneProduct { @Override public void start() { System.out.println("开启小米手机"); } @Override public void shutdown() { System.out.println("关闭小米手机"); } @Override public void callup() { System.out.println("小米手机打电话"); } @Override public void sendSMS() { System.out.println("小米手机发短信"); } }
-
IRouterProduct
package com.kmu.shu.abstractFactory; //路由器产品接口 public interface IRouterProduct { //开机 void start(); //关机 void shutdown(); //打开WiFi void openWiFi(); //设置参数 void setting(); }
-
HuaweiRouter
package com.kmu.shu.abstractFactory; //华为路由器 public class HuaweiRouter implements IRouterProduct{ @Override public void start() { System.out.println("开启华为路由器"); } @Override public void shutdown() { System.out.println("关闭华为路由器"); } @Override public void openWiFi() { System.out.println("打开华为WiFi"); } @Override public void setting() { System.out.println("设置华为路由器"); } }
-
XiaomiRouter
package com.kmu.shu.abstractFactory; //小米路由器 public class XiaomiRouter implements IRouterProduct{ @Override public void start() { System.out.println("开启小米路由器"); } @Override public void shutdown() { System.out.println("关闭小米路由器"); } @Override public void openWiFi() { System.out.println("打开小米WiFi"); } @Override public void setting() { System.out.println("设置小米路由器"); } }
-
IProductFactory
package com.kmu.shu.abstractFactory; //抽象产品工厂:抽象产品工厂生产抽象产品 public interface IProductFactory { //生产手机 IPhoneProduct iPhoneProduct(); //生产路由器 IRouterProduct iRouterProduct(); }
-
XiaomiFactory
package com.kmu.shu.abstractFactory; //小米工厂 public class XiaomiFactory implements IProductFactory{ @Override public IPhoneProduct iPhoneProduct() { //直接生产一个小米手机:new XiaomiPhone() return new XiaomiPhone(); } @Override public IRouterProduct iRouterProduct() { //直接生产一个小米路由器:new XiaomiRouter() return new XiaomiRouter(); } }
-
HuaweiFactory
package com.kmu.shu.abstractFactory; public class HuaweiFactory implements IProductFactory{ @Override public IPhoneProduct iPhoneProduct() { //直接生产一个华为手机 return new HuaweiPhone(); } @Override public IRouterProduct iRouterProduct() { //直接生产一个华为路由器 return new HuaweiRouter(); } }
-
Client
package com.kmu.shu.abstractFactory; public class Client { public static void main(String[] args) { System.out.println("==============小米系列产品================="); //小米工厂 XiaomiFactory xiaomiFactory = new XiaomiFactory(); //小米工厂生产一个小米手机 IPhoneProduct xiaomiPhone = xiaomiFactory.iPhoneProduct(); //启动小米手机 xiaomiPhone.start(); xiaomiPhone.callup(); //小米工厂生产一个小米路由器 IRouterProduct xiaomiRouter = xiaomiFactory.iRouterProduct(); xiaomiRouter.start(); xiaomiRouter.openWiFi(); System.out.println("==============华为系列产品================="); //华为工厂 HuaweiFactory huaweiFactory = new HuaweiFactory(); //华为工厂生产华为手机 IPhoneProduct huaweiPhone = huaweiFactory.iPhoneProduct(); huaweiPhone.start(); huaweiPhone.callup(); //华为工厂生产一个华为路由器 IRouterProduct huaweiRouter = huaweiFactory.iRouterProduct(); huaweiRouter.start(); huaweiRouter.openWiFi(); } }
-
类图分析
-
若需生产笔记本产品,则需要修改代码,不满足开闭原则!不可以增加产品,可以增加产品族!
package com.kmu.shu.abstractFactory; //抽象产品工厂:抽象产品工厂生产抽象产品 public interface IProductFactory { //生产手机 IPhoneProduct iPhoneProduct(); //生产路由器 IRouterProduct iRouterProduct(); //若需生产笔记本产品,则需要修改代码,不满足开闭原则 //ILaptopProduct iLaptopProduct(); }
2.2.5 工厂模式小结
-
简单工厂模式(静态工厂模式)
- 虽然某种程度上不符合设计原则,但实际使用最多!
-
工厂方法模式
- 不修改已有类的前提下,通过增加新的工厂类实现扩展
-
抽象工厂模式
- 不可以增加产品,可以增加产品族!
-
应用场景:
- JDK中的Calendar的getInstance方法
- JDBC中的Connection对象的获取
- Spring中IOC容器创建管理Bean对象
- 反射中Class对象的newInstance方法
2.3 建造者模式
建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式。
**定义:**将一个复杂对象的构建与它得到表示分离,使得同样的构建过程可以创建不同的表示(例如造房子,一样的过程可以建造平房、高楼、四合院等等)
主要作用:
-
在用户不知道对象的建造和细节的情况下就可以直接创建复杂的对象
-
在用户只需要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)
例子:
- 工厂(建造者模式):负责制造汽车(组装过程和细节在工厂内)
- 汽车购买者(用户):你只需要说出你需要的型号(对象的类型和内容),然后直接购买就可以使用了(不需要知道汽车是怎么组装的(车轮、车门、发动机、方向盘等等))
2.3.1 Builder模式的常规用法(Director)
-
角色分析
-
既然是建造者模式,那么我们还是继续造房子吧,其实我也想不到更简单的例子。假设造房简化为如下步骤:
- 地基
- 钢筋工程
- 铺电线
- 粉刷
如果要盖一座房子,首先要找一个建筑公司或者工程承包商(指挥者)。承包商通过图纸(房屋结构)指挥工人(具体建造者)过来造房子(产品),最后验收。
-
Builder
package com.kmu.shu.builder; //抽象的建造者:不负责建房子,只负责定义一些方法和接口 public abstract class Builder { abstract void buildA(); //地基 abstract void buildB(); //钢筋工程 abstract void buildC(); //铺电线 abstract void buildD(); //粉刷 //完工:得到产品 abstract Product getProduct(); }
-
Product
package com.kmu.shu.builder; //产品:房子 public class Product { private String buildA; private String buildB; private String buildC; private String buildD; public String getBuildA() { return buildA; } public void setBuildA(String buildA) { this.buildA = buildA; } public String getBuildB() { return buildB; } public void setBuildB(String buildB) { this.buildB = buildB; } public String getBuildC() { return buildC; } public void setBuildC(String buildC) { this.buildC = buildC; } public String getBuildD() { return buildD; } public void setBuildD(String buildD) { this.buildD = buildD; } @Override public String toString() { return "Product{" + "buildA='" + buildA + '\'' + ", buildB='" + buildB + '\'' + ", buildC='" + buildC + '\'' + ", buildD='" + buildD + '\'' + '}'; } }
-
Worker
package com.kmu.shu.builder; //具体的建造者:工人,继承抽象类Builder public class Worker extends Builder{ private Product product; //工人创建一个产品 public Worker() { product = new Product(); } @Override void buildA() { product.setBuildA("地基"); System.out.println("地基"); } @Override void buildB() { product.setBuildB("钢筋工程"); System.out.println("钢筋工程"); } @Override void buildC() { product.setBuildC("铺电线"); System.out.println("铺电线"); } @Override void buildD() { product.setBuildD("粉刷"); System.out.println("粉刷"); } @Override Product getProduct() { return product; } }
-
Director
package com.kmu.shu.builder; //指挥:核心。负责指挥构建一个工程,工程如何构建,由它决定 public class Director { //指挥,指挥工人按照顺序建房子 public Product build(Builder builder){ builder.buildA(); builder.buildB(); builder.buildC(); builder.buildD(); return builder.getProduct(); } }
-
Test
package com.kmu.shu.builder; public class Test { public static void main(String[] args) { //指挥 Director director = new Director(); //指挥具体的工人完成产品 Product build = director.build(new Worker()); build.toString(); } }
-
小结
- 上面示例是Builder模式的常规用法,导演类Director在Builder模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把Director和抽象建造者进行结合。
- 通过静态内部类方式实现零件无序装配构造,这种方式使用更加灵活,更符合定义。内部有复杂对象的默认实现,使用时可以根据用户需求自由定义更改内容,并且无需改变具体的构造方式。就可以生产出不同复杂产品
- 比如:比如麦当劳的套餐,服务员(具体建造者)可以随意搭配任意几种产品(零件)组成一款套餐(产品),然后出售给客户。比第一种方式少了指挥者,主要是因为第二种方式把指挥者交给用户来操作,使得产品的创建更加简单灵活。
2.3.2 把指挥者交给用户来操作
-
Builder
package com.kmu.shu.builder.demo02; //建造者 public abstract class Builder { abstract Builder buildA(String msg); //汉堡 abstract Builder buildB(String msg); //可乐 abstract Builder buildC(String msg); //薯条 abstract Builder buildD(String msg); //甜点 //套餐 abstract Product getProduct(); }
-
Product
package com.kmu.shu.builder.demo02; //产品:套餐 public class Product { //用户不做选择:默认套餐 用户自己选择:自定义套餐 private String buildA="汉堡"; private String buildB="可乐"; private String buildC="薯条"; private String buildD="甜点"; public String getBuildA() { return buildA; } public void setBuildA(String buildA) { this.buildA = buildA; } public String getBuildB() { return buildB; } public void setBuildB(String buildB) { this.buildB = buildB; } public String getBuildC() { return buildC; } public void setBuildC(String buildC) { this.buildC = buildC; } public String getBuildD() { return buildD; } public void setBuildD(String buildD) { this.buildD = buildD; } @Override public String toString() { return "Product{" + "buildA='" + buildA + '\'' + ", buildB='" + buildB + '\'' + ", buildC='" + buildC + '\'' + ", buildD='" + buildD + '\'' + '}'; } }
-
Worker
package com.kmu.shu.builder.demo02; //具体的建造者 public class Worker extends Builder{ private Product product; //建造者创建自己的产品 public Worker() { product = new Product(); } @Override Builder buildA(String msg) { product.setBuildA(msg); //将当前对象返回给它 return this; } @Override Builder buildB(String msg) { product.setBuildA(msg); //将当前对象返回给它 return this; } @Override Builder buildC(String msg) { product.setBuildB(msg); //将当前对象返回给它 return this; } @Override Builder buildD(String msg) { product.setBuildC(msg); //将当前对象返回给它 return this; } @Override Product getProduct() { return product; } }
-
Test
package com.kmu.shu.builder.demo02; public class Test { public static void main(String[] args) { //服务员 Worker worker = new Worker(); //服务员创建自己的产品:默认不做选择 Product product = worker.getProduct(); System.out.println(product.toString()); } }
-
用户自定义套餐
package com.kmu.shu.builder.demo02; public class Test { public static void main(String[] args) { /*//服务员 Worker worker = new Worker(); //服务员创建自己的产品:默认不做选择 Product product = worker.getProduct(); System.out.println(product.toString());*/ //服务员 Worker worker = new Worker(); //链式编程:在原来的基础上可以自由组合了,如果不组合也有默认的套餐 Product product = worker.buildA("全家桶").buildB("雪碧") .getProduct(); System.out.println(product.toString()); } }
2.3.3 建造者模式小结
-
优点
(1)产品的建造和表示分离,实现了解耦。使用建造者模式可以使客户端不必知道产品内部组成的细节。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰
(2)具体的建造者类之间是相互独立的,这有利于系统的扩展。增加新的具体建造者无需修改原有类库的代码,符合“开闭原则“。 -
缺点
(1)建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
(2)如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。 -
应用场景
(1)需要生成的产品对象有复杂的内部结构,这些产品对象具备共性;
(2)隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。适合于一个具有较多的零件(属性)的产品(对象)的创建过程。 -
建造者与抽象工厂模式的比较
(1)与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族
(2)在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
(3)如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车!
2.4 原型模式
- 克隆:多莉羊
- Prototype
- Cloneable接口
- Clone()方法
原型模式
(1)浅克隆:
-
Video
package com.kmu.shu.prototype.demo01; import java.util.Date; /* * 1、实现一个接口 Cloneable * 2、重写一个方法 clone() * */ //Video:视频类 public class Video implements Cloneable{//无良up主,克隆别人的视频! private String name; private Date creatTime; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getCreatTime() { return creatTime; } public void setCreatTime(Date creatTime) { this.creatTime = creatTime; } public Video(String name, Date creatTime) { this.name = name; this.creatTime = creatTime; } public Video() { } @Override public String toString() { return "Video{" + "name='" + name + '\'' + ", creatTime=" + creatTime + '}'; } }
-
Bilibili
package com.kmu.shu.prototype.demo01; import java.util.Date; //客户端:克隆 public class Bilibili { public static void main(String[] args) throws CloneNotSupportedException { //原型对象:v1 Date date = new Date(); Video v1 = new Video("小舒的Vlog",date); System.out.println("v1=>"+v1); System.out.println("v1=>hash:"+v1.hashCode()); //v1 克隆 v2 //Video v2 = new Video("小舒的Vlog",date); //原来的操作 //Object clone = v1.clone(); Video v2 = (Video)v1.clone(); //将Object对象强制转换为Video对象 System.out.println("v2=>"+v2); System.out.println("v2=>hash:"+v2.hashCode()); v2.setName("Clone:小舒的Vlog"); System.out.println("v2=>"+v2); /* 输出:hashCode不一样说明是两个对象 v1=>Video{name='小舒的Vlog', creatTime=Fri Oct 07 15:30:27 CST 2022} v1=>hash:1735600054 v2=>Video{name='小舒的Vlog', creatTime=Fri Oct 07 15:30:27 CST 2022} v2=>hash:21685669 v2=>Video{name='Clone:小舒的Vlog', creatTime=Fri Oct 07 15:30:27 CST 2022}*/ } }
-
浅克隆,用的是同一个date对象,若改变v1的属性,v2也会跟着变
package com.kmu.shu.prototype.demo02; import java.util.Date; //客户端:克隆 public class Bilibili { public static void main(String[] args) throws CloneNotSupportedException { //原型对象:v1 Date date = new Date(); Video v1 = new Video("小舒的Vlog",date); Video v2 = (Video)v1.clone(); //将Object对象强制转换为Video对象 System.out.println("v1=>"+v1); System.out.println("v2=>"+v2); System.out.println("============================"); date.setTime(2050); System.out.println("v1=>"+v1); System.out.println("v2=>"+v2); /*v1=>Video{name='小舒的Vlog', creatTime=Fri Oct 07 15:38:47 CST 2022} v2=>Video{name='小舒的Vlog', creatTime=Fri Oct 07 15:38:47 CST 2022} ============================ v1=>Video{name='小舒的Vlog', creatTime=Thu Jan 01 08:00:02 CST 1970} v2=>Video{name='小舒的Vlog', creatTime=Thu Jan 01 08:00:02 CST 1970}*/ } }
(2)深克隆:改变v1的属性,v2不会改变
-
Video
package com.kmu.shu.prototype.demo02; import java.util.Date; //Video:视频类 public class Video implements Cloneable{//无良up主,克隆别人的视频! private String name; private Date creatTime; @Override //深克隆:需要改变clone()方法,除了这个就是序列化与反序列化 protected Object clone() throws CloneNotSupportedException { Object obj = super.clone(); Video v = (Video) obj; //将这个对象v的属性也进行克隆 //this:表示当前类 //this.creatTime表示当前时间对象 v.creatTime = (Date) this.creatTime.clone(); return obj; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getCreatTime() { return creatTime; } public void setCreatTime(Date creatTime) { this.creatTime = creatTime; } public Video(String name, Date creatTime) { this.name = name; this.creatTime = creatTime; } public Video() { } @Override public String toString() { return "Video{" + "name='" + name + '\'' + ", creatTime=" + creatTime + '}'; } }
-
Bilibili
package com.kmu.shu.prototype.demo02; import java.util.Date; //Spring Bean的创建: // 1、单例模式创建 // 2、原型模式创建:通过拷贝来实现,不用重复的创造,从而节省了一些空间 public class Bilibili { public static void main(String[] args) throws CloneNotSupportedException { //原型对象:v1 Date date = new Date(); Video v1 = new Video("小舒的Vlog",date); Video v2 = (Video)v1.clone(); //将Object对象强制转换为Video对象 System.out.println("v1=>"+v1); System.out.println("v2=>"+v2); System.out.println("============================"); date.setTime(2050); System.out.println("v1=>"+v1); System.out.println("v2=>"+v2); } }
三、结构型模式
3.1 代理模式
代理模式的分类:
- 静态代理
- 动态代理
3.1.1 静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
- 客户:访问代理对象的人
代码步骤:
-
接口
//租房 public interface Rent { public void rent(); }
-
真实角色
//房东 public class Host implements Rent{ @Override public void rent() { System.out.println("房东要出租房子"); } }
-
代理角色
//中介 public class Proxy implements Rent{ private Host host; //使用组合方式:多用组合,少用继承 public Proxy() { } public Proxy(Host host) { this.host = host; } @Override public void rent() { seeHouse(); contract(); fare(); host.rent(); //帮房东租房子 } //看房 public void seeHouse(){ System.out.println("中介带你看房"); } //签合同 public void contract(){ System.out.println("签租赁合同"); } //收中介费 public void fare(){ System.out.println("收中介费"); } }
-
客户端访问代理角色
//客户 public class Client { public static void main(String[] args) { //房东要租房子 Host host = new Host(); //代理,中介帮房东租房子,但是呢?代理角色一般会有一些附属操作 Proxy proxy = new Proxy(host); //你不用面对房东,直接找中介租房即可! proxy.rent(); } }
代理模式的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务也就交给了代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
缺点:一个真实角色就会产生一个代理角色;代码量会翻倍,开发效率会变低
3.1.2加深理解
代码步骤:
-
接口
public interface UserService { public void add(); public void delete(); public void update(); public void query(); }
-
真实角色
//真实对象 public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("增加了一个用户"); } @Override public void delete() { System.out.println("删除了一个用户"); } @Override public void update() { System.out.println("修改了一个用户"); } @Override public void query() { System.out.println("查询了一个用户"); } }
-
代理角色
/* 改动原有的业务代码,在公司中是大忌! */ public class UserServiceProxy implements UserService{ private UserServiceImpl userService; //spring注入一个对象建议使用set方法 public void setUserService(UserServiceImpl userService) { this.userService = userService; } @Override public void add() { log("add"); userService.add(); } @Override public void delete() { log("delete"); userService.delete(); } @Override public void update() { log("update"); userService.update(); } @Override public void query() { log("query"); userService.query(); } //日志方法 public void log(String msg){ System.out.println("使用了"+msg+"方法"); } }
-
客户端访问
public class Client { public static void main(String[] args) { UserServiceImpl userService = new UserServiceImpl(); UserServiceProxy userServiceProxy = new UserServiceProxy(); userServiceProxy.setUserService(userService); userServiceProxy.add(); } }
3.1.3动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口——JDK动态代理 【我们在这里使用】
- 基于类——cglib
- java字节码实现:javasist
需要了解两个类:Proxy:代理 InvocationHandler:调用处理程序
代码步骤:
-
接口
//租房 public interface Rent { public void rent(); }
-
真实角色
//房东 public class Host implements Rent { @Override public void rent() { System.out.println("房东要出租房子了"); } }
-
代理调用处理程序
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //等会我们会用这个类,自动生成代理类! public class ProxyInvocationHandler implements InvocationHandler { //被代理的接口 private Rent rent; public void setRent(Rent rent) { this.rent = rent; } //生成得到代理类直接通过newProxyInstance:这段代码是死的!只需要修改被代理的接口 rent.getClass().getInterfaces()参数 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this); } //处理代理实例,并返回结果 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //调用代理类之前执行seeHouse()方法 seeHouse(); //动态代理的本质就是使用反射机制实现 Object result = method.invoke(rent, args); //调用代理类之后执行fare()方法 fare(); return result; } public void seeHouse(){ System.out.println("中介带看房子"); } public void fare(){ System.out.println("收中介费"); } }
-
客户端访问
public class Client { public static void main(String[] args) { //真实角色 Host host = new Host(); //代理角色:现在没有,需要使用ProxyInvocationHandler类生成 ProxyInvocationHandler pih = new ProxyInvocationHandler(); //通过调用程序处理角色来处理我们要调用的接口对象! pih.setRent(host); //动态生成代理类proxy,我们并没有写它 Rent proxy = (Rent) pih.getProxy(); //使用代理类调用方法 proxy.rent(); } }
动态代理的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务也就交给了代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
- 一个动态代理类代理的是一个接口,一般就是对应的一类业务
- 一个动态代理类可以代理多个类,只要实现了同一个接口即可!
3.1.4加深理解
代码步骤:
-
生成模板类(直接套用即可)
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //等会我们会用这个类,自动生成代理类! public class ProxyInvocationHandler implements InvocationHandler { //被代理的接口 private Object target; public void setTarget(Object target) { this.target = target; } //生成得到代理类直接通过newProxyInstance:这段代码是死的!只需要修改被代理的接口 rent.getClass().getInterfaces()参数 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this); } //处理代理实例,并返回结果 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ log(method.getName()); //动态代理的本质就是使用反射机制实现 Object result = method.invoke(target, args); return result; } public void log(String msg){ System.out.println("执行了"+msg+"方法"); } }
-
客户访问类(只需要修改此类即可实现)
public class Client { public static void main(String[] args) { //真实角色 UserServiceImpl userService = new UserServiceImpl(); //代理角色,不存在 ProxyInvocationHandler pih = new ProxyInvocationHandler(); pih.setTarget(userService); //设置要代理的对象 //动态生成代理类 UserService proxy = (UserService) pih.getProxy(); proxy.delete(); } }
3.2 桥接模式
3.2.1 桥接模式
**定义:**桥接模式(bridge)是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。
**分析:**这个场景中有两个变化的维度:品牌,类型;多继承结构耦合性太强了,而且不符合单一职责原则
实现:
-
Brand
package com.kmu.shu.bridge; //品牌 public interface Brand { void info(); }
-
Lenovo
package com.kmu.shu.bridge; //联想品牌 public class Lenovo implements Brand{ @Override public void info() { System.out.print("联想"); } }
-
Apple
package com.kmu.shu.bridge; public class Apple implements Brand{ @Override public void info() { System.out.print("苹果"); } }
-
Computer:桥的作用
package com.kmu.shu.bridge; //抽象的电脑类型类 桥 public abstract class Computer { //组合,电脑出厂自带品牌 protected Brand brand; //私有的只有本类可以访问,protected,当子类继承父类的时候便可以访问 public Computer(Brand brand) { this.brand = brand; } public void info(){ //自带品牌 brand.info(); } } class Desktop extends Computer{ //有这个构造方法,调用的时候就可以调用父类的构造方法 public Desktop(Brand brand) { super(brand); } @Override public void info(){ super.info(); System.out.println("台式机"); } } class Laptop extends Computer{ //有这个构造方法,调用的时候就可以调用父类的构造方法 public Laptop(Brand brand) { super(brand); } @Override public void info(){ super.info(); System.out.println("笔记本"); } }
-
Test
package com.kmu.shu.bridge; public class Test { public static void main(String[] args) { //苹果笔记本 Computer appleLaptop = new Laptop(new Apple()); appleLaptop.info(); //联想台式机 Computer lenovoDesktop = new Desktop(new Lenovo()); lenovoDesktop.info(); } }
-
画图分析:避免了多继承,可以随意扩展
3.2.2 桥接模式小结
-
优点
(1)桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法。极大的减少了子类的个数,从而降低管理和维护的成本
(2)桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则,就像一座桥,可以把两个变化的维度连接起来! -
缺点
(1)桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
(2)桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。 -
最佳实践
(1)如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
(2)一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
(3)虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 -
应用场景
(1)Java语言通过Java虚拟机实现了平台的无关性
(2)AWT中的Peer架构
(3)JDBC驱动程序也是桥接模式的应用之一
3.3 装饰者模式
3.3.1 概述
我们先来看一个快餐店的例子。
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
使用继承的方式存在的问题:
-
扩展性不好
如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。
-
产生过多的子类
定义:
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
结构
装饰(Decorator)模式中的角色:
- 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
3.3.2案例
我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。
类图如下:

代码如下:
//快餐接口
public abstract class FastFood {
private float price;
private String desc;
public FastFood() {
}
public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}
public void setPrice(float price) {
this.price = price;
}
public float getPrice() {
return price;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public abstract float cost(); //获取价格
}
//炒饭
public class FriedRice extends FastFood {
public FriedRice() {
super(10, "炒饭");
}
public float cost() {
return getPrice();
}
}
//炒面
public class FriedNoodles extends FastFood {
public FriedNoodles() {
super(12, "炒面");
}
public float cost() {
return getPrice();
}
}
//配料类
public abstract class Garnish extends FastFood {
private FastFood fastFood;
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
public Garnish(FastFood fastFood, float price, String desc) {
super(price,desc);
this.fastFood = fastFood;
}
}
//鸡蛋配料
public class Egg extends Garnish {
public Egg(FastFood fastFood) {
super(fastFood,1,"鸡蛋");
}
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
//培根配料
public class Bacon extends Garnish {
public Bacon(FastFood fastFood) {
super(fastFood,2,"培根");
}
@Override
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
//测试类
public class Client {
public static void main(String[] args) {
//点一份炒饭
FastFood food = new FriedRice();
//花费的价格
System.out.println(food.getDesc() + " " + food.cost() + "元");
System.out.println("========");
//点一份加鸡蛋的炒饭
FastFood food1 = new FriedRice();
food1 = new Egg(food1);
//花费的价格
System.out.println(food1.getDesc() + " " + food1.cost() + "元");
System.out.println("========");
//点一份加培根的炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
//花费的价格
System.out.println(food2.getDesc() + " " + food2.cost() + "元");
}
}
好处:
-
饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
-
装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
3.3.3 使用场景
-
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
- 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 第二类是因为类定义不能继承(如final类)
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
3.3.4 JDK源码解析
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。
我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter
public class Demo {
public static void main(String[] args) throws Exception{
//创建BufferedWriter对象
//创建FileWriter对象
FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt");
BufferedWriter bw = new BufferedWriter(fw);
//写数据
bw.write("hello Buffered");
bw.close();
}
}
使用起来感觉确实像是装饰者模式,接下来看它们的结构:

小结:
BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。
3.3.5代理和装饰者的区别
静态代理和装饰者模式的区别:
- 相同点:
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
- 不同点:
- 目的不同
装饰者是为了增强目标对象
静态代理是为了保护和隐藏目标对象 - 获取目标对象构建的地方不同
装饰者是由外界传递进来,可以通过构造方法传递
静态代理是在代理类内部创建,以此来隐藏目标对象
- 目的不同
3.4 适配器模式
3.4.1 概述
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h4SOI4Jj-1668053334547)(https://kevin-1311972042.cos.ap-chengdu.myqcloud.com/img/%E8%BD%AC%E6%8E%A5%E5%A4%B4.png)]
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
3.4.2 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
3.4.3 类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
类图如下:

代码如下:
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}
//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
//电脑类
public class Computer {
public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}
//TF卡实现类
public class TFCardImpl implements TFCard {
public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
//定义适配器类(SD兼容TF)
public class SDAdapterTF extends TFCardImpl implements SDCard {
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
3.4.4 对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:

代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
//创建适配器对象(SD兼容TF)
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
TFCard tfCard = new TFCardImpl();
SDAdapterTF adapter = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(adapter));
}
}
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
3.4.5 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
3.4.6 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9vkQNLD1-1668053334547)(https://kevin-1311972042.cos.ap-chengdu.myqcloud.com/img/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F-jdk%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.png)]
从上图可以看出:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。
3.5 外观模式
3.5.1 概述
有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。
定义:
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则”的典型应用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEodusNL-1668053334547)(https://kevin-1311972042.cos.ap-chengdu.myqcloud.com/img/%E5%A4%96%E8%A7%82%E6%A8%A1%E5%BC%8F%E5%BC%95%E5%85%A5.jpg)]
3.5.2 结构
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
3.5.3 案例
【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:

代码如下:
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}
public void off() {
System.out.println("关闭了灯....");
}
}
//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}
public void off() {
System.out.println("关闭了电视....");
}
}
//控制类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}
public void off() {
System.out.println("关闭了空调....");
}
}
//智能音箱
public class SmartAppliancesFacade {
private Light light;
private TV tv;
private AirCondition airCondition;
public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}
public void say(String message) {
if(message.contains("打开")) {
on();
} else if(message.contains("关闭")) {
off();
} else {
System.out.println("我还听不懂你说的!!!");
}
}
//起床后一键开电器
private void on() {
System.out.println("起床了");
light.on();
tv.on();
airCondition.on();
}
//睡觉一键关电器
private void off() {
System.out.println("睡觉了");
light.off();
tv.off();
airCondition.off();
}
}
//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}
好处:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
3.5.4 使用场景
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
3.5.5 源码解析
使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。

RequestFacade类就使用了外观模式。先看结构图:

为什么在此处使用外观模式呢?
定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。
3.6 组合模式
3.6.1 概述

对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。
定义:
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
3.6.2 结构
组合模式主要包含三种角色:
- 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
- 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
3.6.3 案例实现
【例】软件菜单
如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

要实现该案例,我们先画出类图:

代码实现:
不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。
//菜单组件:抽象根节点
public abstract class MenuComponent {
//菜单组件的名称
protected String name;
protected int level;
//添加子菜单功能
public void add(MenuComponent menuComponent){
throw new UnsupportedOperationException();//菜单项不能使用该方法
}
//移除子菜单功能
public void remove(MenuComponent menuComponent){
throw new UnsupportedOperationException();//菜单项不能使用该方法
}
//获取指定的子菜单,需要传一个索引
public MenuComponent getChild(int index){
throw new UnsupportedOperationException();
}
//获取菜单或者菜单项的名称
public String getName(){
return name;
}
//打印菜单名称的方法(包含子菜单和菜单项)
public abstract void print();
}
这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。
import java.util.ArrayList;
import java.util.List;
//菜单类:树枝节点角色
public class Menu extends MenuComponent{
//菜单可以有多个子菜单或者菜单项
private List<MenuComponent> menuComponentList = new ArrayList<MenuComponent>();
//构造方法
public Menu(String name,int level) {
this.name = name;
this.level = level;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}
@Override
public MenuComponent getChild(int index) {
return menuComponentList.get(index);
}
@Override
public void print() {
//打印菜单名称
for (int i=0;i<level;i++){
System.out.print("--");
}
System.out.println(name);
//打印子菜单或者子菜单项的名称
for (MenuComponent menuComponent : menuComponentList) {
menuComponent.print();
}
}
}
Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。
//菜单项类:叶子结点角色
public class MenuItem extends MenuComponent{
public MenuItem(String name,int level){
this.name = name;
this.level = level;
}
@Override
public void print() {
//打印菜单项的名称
for (int i=0;i<level;i++){
System.out.print("--");
}
System.out.println(name);
}
}
MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。
Client类
package com.kmu.shu.combination;
public class Cilent {
public static void main(String[] args) {
//创建菜单树
MenuComponent menu1 = new Menu("菜单管理",2);
menu1.add(new MenuItem("页面访问",3));
menu1.add(new MenuItem("展开菜单",3));
menu1.add(new MenuItem("编辑菜单",3));
menu1.add(new MenuItem("新增菜单",3));
menu1.add(new MenuItem("删除菜单",3));
MenuComponent menu2 = new Menu("权限管理",2);
menu2.add(new MenuItem("页面访问",3));
menu2.add(new MenuItem("提交保存",3));
MenuComponent menu3 = new Menu("角色管理",2);
menu3.add(new MenuItem("页面访问",3));
menu3.add(new MenuItem("新增角色",3));
menu3.add(new MenuItem("修改角色",3));
//创建一级菜单
MenuComponent component = new Menu("系统管理",1);
//将二级菜单添加到一级菜单中
component.add(menu1);
component.add(menu2);
component.add(menu2);
//打印菜单名称(如果有子菜单,一起打印出来)
component.print();
}
}
3.6.4 组合模式的分类
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
-
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
MenuComponent
声明了add
、remove
、getChild
方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
-
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点
Menu
类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
3.6.5 优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
3.6.6 使用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
3.7 享元模式
3.7.1 概述
定义:
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
3.7.2 结构
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
3.7.3 案例实现
【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

先来看类图:

代码如下:
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
public abstract class AbstractBox {
public abstract String getShape();
public void display(String color) {
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
}
}
接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。
public class BoxFactory {
private static HashMap<String, AbstractBox> map;
private BoxFactory() {
map = new HashMap<String, AbstractBox>();
AbstractBox iBox = new IBox();
AbstractBox lBox = new LBox();
AbstractBox oBox = new OBox();
map.put("I", iBox);
map.put("L", lBox);
map.put("O", oBox);
}
public static final BoxFactory getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final BoxFactory INSTANCE = new BoxFactory();
}
public AbstractBox getBox(String key) {
return map.get(key);
}
}
3.7.5 优缺点和使用场景
1,优点
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
2,缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
3,使用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
3.7.6 JDK源码解析
Integer类使用了享元模式。我们先看下面的例子:
public class Demo {
public static void main(String[] args) {
Integer i1 = 127;
Integer i2 = 127;
System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));
Integer i3 = 128;
Integer i4 = 128;
System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
}
}
运行上面代码,结果如下:

为什么第一个输出语句输出的是true,第二个输出语句输出的是false?通过反编译软件进行反编译,代码如下:
public class Demo {
public static void main(String[] args) {
Integer i1 = Integer.valueOf((int)127);
Integer i2 Integer.valueOf((int)127);
System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
Integer i3 = Integer.valueOf((int)128);
Integer i4 = Integer.valueOf((int)128);
System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
}
}
上面代码可以看到,直接给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf()
,所以只需要看该方法即可
public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}
可以看到 Integer
默认先创建并缓存 -128 ~ 127
之间数的 Integer
对象,当调用 valueOf
时如果参数在 -128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的 Integer
对象。