结构型设计模式(7种)
概述
结构型设计模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。
分类
结构型(7种):
介绍如何将对象和类组成较大的结构,并保持结构的灵活和高效
· 常用:
代理模式、桥接模式、装饰者模式、适配器模式。
· 不常用:
外观模式、组合模式、享元模式
1、代理模式
1.1、定义
让你能通过提供对象的替代品或占位符,代理控制对于原对象的访问,并允许将请求提交给对象前后进行一些处理。
1.2、特点
通过代理对象访问目标对象,在不改变目标对象的情况下,控制访问并拓展功能。
1.3、代理模式UML图
1.4、静态代理
1.4.1、特点
在编译时就已经实现了,编译完成后代理类是一个实际的class文件。
1.4.2、静态代理实现
实现:利用UserDaoProxy代理IUserDao的实现类,从而达到控制访问或增强功能的目的。
IUserDao接口(Target):
public interface IUserDao {
void save();
}
UserDaoImpl实现类(RealTarget):
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
UserDaoProxy实现类(Proxy):
public class UserDaoProxy implements IUserDao {
private IUserDao target;
public UserDaoProxy(IUserDao userDao) {
this.target = userDao;
}
@Override
public void save() {
System.out.println("代理对象开始代理");
target.save();
System.out.println("代理对象结束代理");
}
}
1.4.2、静态代理测试代码
通过传入的IUserDao具体实现类,获取代理对象,再调用代理方法。
public class TestProxy {
@Test
public void test01() {// 静态代理
IUserDao userDao = new UserDaoImpl();
UserDaoProxy proxy = new UserDaoProxy(userDao);
proxy.save();
}
}
1.5、动态代理
1.5.1、特点
动态地在内存中构建代理对象,从而实现对目标对象的代理功能。
(动态代理是运行时动态生成,即编译完成后没有实际 的class文件,而是在运行时动态生成类字节码,并加载到JVM中。)
1.5.2、JDK动态代理
1.5.2.1、JDK动态代理实现
通过代理IUserDao实现类,增强save方法。(如果有多个方法可通过method.getName()匹配,单独增强)。
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
loader:
目标对象使用的类加载器
interfaces:
目标对象实现的接口类型
h:
事件处理器
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标对象使用的类加载器
target.getClass().getInterfaces(), // 目标对象实现的接口类型
new InvocationHandler() { // 事件处理器
/**
* @param proxy 代理对象
* @param method 对应于代理对象调用的接口实例
* @param args 对应了代理对象调用接口方法传递的实际参数
* @return java.lang.Object 返回目标对象的方法的返回值,没有返回值返回 null
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开启事务");
Object o = method.invoke(target, args);
System.out.println("结束事务");
return o;
}
}
);
}
}
1.5.2.2、JDK动态代理测试代码
通过传入的IUserDao具体实现类,获取代理对象,再调用代理方法。
public class TestProxy {
@Test
public void test02() {//JDK动态代理
IUserDao userDao = new UserDaoImpl();
System.out.println("userDao.getClass():" + userDao.getClass());
IUserDao proxy = (IUserDao) new ProxyFactory(userDao).getProxyInstance();
System.out.println("proxy.getClass():" + proxy.getClass());
proxy.save();// 代理方法
}
}
事件处理器:在代理对象调用代理方法时,才会走invoke。
此时,调用了proxy.save();
proxy:代理对象,UserDao的实现类UserDaoImpl
method:save的Method对象
args:传参为空
返回值为空
1.5.2.3、JDK动态代理测试效果
1.5.3、CGLIB动态代理
1.5.3.1、CGLIB动态代理实现
添加CGLIB依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
User类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
}
UserService接口(Target):
public interface UserService {
public List<User> getUsers();
}
UserServiceImpl实现类(RealTarget):
public class UserServiceImpl implements UserService{
@Override
public List<User> getUsers() {
return Collections.singletonList(new User("李四"));
}
}
UserLogProxy代理类(Proxy):实现MethodInterceptor,重写intercept方法
public class UserLogProxy implements MethodInterceptor {
/**
* 生成 cglib动态代理类
*
* @param target 需要被代理的目标类
* @return 代理类对象
*/
public Object getLogProxy(Object target) {
// 增强器类,用来创建动态代理类
Enhancer enhancer = new Enhancer();
// 设置代理类的父类字节码
enhancer.setSuperclass(target.getClass());
// 设置回调
enhancer.setCallback(this);
// 创建动态代理对象并返回
return enhancer.create();
}
/**
* 实现回调的方法
* @param o 代理对象
* @param method 目标对象方法的 Method实例
* @param args 实际参数
* @param methodProxy 代理类对象中方法的 Method实例
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
LocalDateTime now = LocalDateTime.now();
String nowStr = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// String nowStr = now.format(DateTimeFormatter.BASIC_ISO_DATE);
System.out.println(nowStr + "[" + method.getName() + "]查询用户信息");
Object result = methodProxy.invokeSuper(o, args);
return result;
}
}
1.5.3.2、CGLIB动态代理测试代码
通过Enhancer增强器类,创建UserService 实现类的动态代理,并调用测试增强的查询功能。
public class TestProxy {
@Test
public void test03() {//CGLIB动态代理
// 目标对象
UserService userService = new UserServiceImpl();
System.out.println(userService.getClass());
// 代理对象
UserServiceImpl proxy = (UserServiceImpl) new UserLogProxy().getLogProxy(userService);
System.out.println(proxy.getClass());
List<User> users = proxy.getUsers();
System.out.println("users = " + users);
}
}
1.5.3.3、CGLIB动态代理测试效果
1.6、总结
代理模式优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
应用场景
-
功能增强
当需要对一个对象的访问提供一些额外操作时,可以使用代理模式
-
远程(Remote)代理
实际上,RPC 框架也可以看作一种代理模式,GoF 的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用 RPC 服务的时候,就像使用本地函数一样,无需了解跟服务器交互的细节。除此之外,RPC 服务的开发者也只需要开发业务逻辑,就像开发本地使用的函数一样,不需要关注跟客户端的交互细节。
-
防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
-
保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
2、桥接模式
2.1、定义
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联来取代传统的多层继承,将类之间的静态继承关系转变为动态的组合关系,使得系统更加灵活,并易于扩展,有效的控制了系统中类的个数 (避免了继承层次的指数级爆炸).
2.2、主要角色
1、 抽象化(Abstraction)角色:主要负责定义出该角色的行为,并包含一个对实现化对象的引用。
2、扩展抽象化(RefinedAbstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
3、 实现化(Implementor)角色 :定义实现化角色的接口,包含角色必须的行为和属性,并供扩展抽象化角色调用。
4、具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
2.3、原理
首先要识别出一个类所具有的的两个独立变化维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。
一句话就是:抽象角色引用实现角色。
例:我们拿支付举例,支付模式和支付渠道是支付的两个维度
- 支付方式可以抽象出一个支付方式类,将各种方式支付作为其子类,如下图
-
支付模式与支付方式存在组合关系,可以提供支付模式接口,将具体的模式作为实现类。
2.3、实现
IPayMode:支付模式接口
public interface IPayMode {
boolean security(String uid);
}
PayModeCypher:支付模式之密码支付
public class PayModeCypher implements IPayMode{
@Override
public boolean security(String uid) {
System.out.println("密码支付");
return true;
}
}
PayModeFingerPrint:支付模式之指纹支付
public class PayModeFingerPrint implements IPayMode{
@Override
public boolean security(String uid) {
System.out.println("指纹支付");
return true;
}
}
PayModeFace:支付模式之人脸支付
public class PayModeFace implements IPayMode{
@Override
public boolean security(String uid) {
System.out.println("人脸支付");
return true;
}
}
Pay:支付方式抽象类
public abstract class Pay {
protected IPayMode payMode;
public Pay(IPayMode payMode){
this.payMode = payMode;
}
public abstract String transfer(String uid, String traceId, BigDecimal amount);
}
WxPay:支付方式之微信支付
public class WxPay extends Pay {
public WxPay(IPayMode payMode) {
super(payMode);
}
@Override
public String transfer(String uid, String traceId, BigDecimal amount) {
System.out.println("微信支付渠道划账开始…………");
boolean b = payMode.security(uid);
System.out.println("微信支付渠道,风险校验:" + uid + "," + traceId + "," + b);
if(!b){
System.out.println("微信渠道支付失败!!");
return "500";
}
System.out.println("微信渠道支付成功!! 金额:" + amount);
return "200";
}
}
ZfbPay:支付方式之支付宝支付
public class ZfbPay extends Pay {
public ZfbPay(IPayMode payMode) {
super(payMode);
}
@Override
public String transfer(String uid, String traceId, BigDecimal amount) {
System.out.println("支付宝支付渠道划账开始…………");
boolean b = payMode.security(uid);
System.out.println("支付宝支付渠道,风险校验:" + uid + "," + traceId + "," + b);
if(!b){
System.out.println("支付宝渠道支付失败!!");
return "500";
}
System.out.println("支付宝渠道支付成功!! 金额:" + amount);
return "200";
}
}
2.4、测试代码
public class TestBridge {
@Test
public void test02() {
Pay wxPay = new WxPay(new PayModeFace());
wxPay.transfer("wx_1001", "100082009", new BigDecimal(200));
System.out.println();
Pay zfbPay = new ZfbPay(new PayModeFingerPrint());
zfbPay.transfer("zfb_1001", "100011009", new BigDecimal(666));
}
}
2.4、测试效果
2.5、总结
桥接模式优缺点
优点:
- 分离抽象接口及其实现部分.桥接模式使用”对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化.
- 在很多情况下,桥接模式可以取代多层继承方案.多层继承方案违背了单一职责原则,复用性差,类的个数多.桥接模式很好的解决了这些问题.
- 桥接模式提高了系统的扩展性,在两个变化维度中任意扩展一个维度都不需要修改原有系统,符合开闭原则.
缺点:
- 桥接模式的使用会增加系统的理解和设计难度,由于关联关系建立在抽象层,要求开发者一开始就要对抽象层进行设计和编程
- 桥接模式要求正确识别出系统中的两个独立变化的维度,因此具有一定的局限性,并且如果正确的进行维度的划分,也需要相当丰富的经验.
应用场景
-
需要提供平台独立性的应用程序时。 比如,不同数据库的 JDBC 驱动程序、硬盘驱动程序等。
-
需要在某种统一协议下增加更多组件时。 比如,在支付场景中,我们期望支持微信、支付宝、各大银行的支付组件等。这里的统一协议是收款、支付、扣款,而组件就是微信、支付宝等。
-
基于消息驱动的场景。 虽然消息的行为比较统一,主要包括发送、接收、处理和回执,但其实具体客户端的实现通常却各不相同,比如,手机短信、邮件消息、QQ 消息、微信消息等。
-
拆分复杂的类对象时。 当一个类中包含大量对象和方法时,既不方便阅读,也不方便修改。
-
希望从多个独立维度上扩展时。 比如,系统功能性和非功能性角度,业务或技术角度等。
3、装饰器模式
3.1、定义
动态的给一个对象添加一些额外的职责。(就扩展功能而言,装饰器模式提供了一种比使用子类更加灵活的替代方案)
假设现在有有一块蛋糕,如果只有涂上奶油那这个蛋糕就是普通的
奶油蛋糕
, 这时如果我们添加上一些蓝莓,那这个蛋糕就是
蓝莓蛋糕
.如果我们再拿一块黑巧克力 然后写上姓名、插上代表年龄的蜡烛, 这就是变成了一块生日蛋糕
3.2、主要角色
- 抽象构件(Component)角色:它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法。它引进了可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
- 具体构件(Concrete Component)角色:它是抽象构件类的子类,用于定义具体的构建对象,实现了在抽象构建中声明的方法,装饰类可以给它增加额外的职责(方法)。
- 抽象装饰(Decorator)角色:它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护了一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
- 具体装饰(ConcreteDecorator)角色: 它是抽象装饰类的子类,负责向构件添加新的职责,每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用于扩充对象的行为。
3.3、原理
装饰类只维护一个指向抽象构件对象的引用,具体扩展(装饰)功能在子类实现。
3.4、实现
DataLoader(装饰目标接口):用于定义数据加载接口
public interface DataLoader {
String read(); // 读操作
void write(String data);// 写操作
}
BaseFileDataLoader(具体的装饰目标实现类):目标的业务实现
public class BaseFileDataLoader implements DataLoader{
private String filePath;
public BaseFileDataLoader(){
}
public BaseFileDataLoader(String filePath){
this.filePath = filePath;
}
@Override
public String read() {
String result = null;
try {
result = FileUtils.readFileToString(new File(filePath),"utf-8");
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
@Override
public void write(String data) {
try {
FileUtils.writeStringToFile(new File(filePath), data, "utf-8");
} catch (IOException e) {
e.printStackTrace();
}
}
}
DataLoaderDecorator(装饰类):只维护一个指向抽象构件对象的引用
public class DataLoaderDecorator implements DataLoader {
private DataLoader dataLoader;
public DataLoaderDecorator(DataLoader dataLoader) {
this.dataLoader = dataLoader;
}
@Override
public String read() {
return dataLoader.read();
}
@Override
public void write(String data) {
dataLoader.write(data);
}
}
EncryptionDataDecorator(具体装饰类):用于拓展(装饰)功能
public class EncryptionDataDecorator extends DataLoaderDecorator {
public EncryptionDataDecorator(DataLoader dataLoader) {
super(dataLoader);
}
@Override
public String read() {
return decode(super.read());
}
@Override
public void write(String data) {
super.write(encode(data));
}
// 加密
public String encode(String data) {
try {
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = new byte[0];
bytes = data.getBytes("utf-8");
return encoder.encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 解密
public String decode(String data) {
Base64.Decoder decoder = Base64.getDecoder();
String res = new String(decoder.decode(data));
return res;
}
}
3.5、测试代码
public class TestDecorator {
@Test
public void test(){
DataLoader dataLoader = new EncryptionDataDecorator(new BaseFileDataLoader("a.txt"));
dataLoader.write("hello decorator!!!");
System.out.println(dataLoader.read());
String read = dataLoader.read();
System.out.println("read = " + read);
}
}
3.6、测试效果
3.7、总结
装饰器模式优缺点
优点:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
- 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为.
- 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合可以创造出很多不同行为的组合,得到更加强大的对象.
- 具体构建类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构建类和具体装饰类,原有类库代码无序改变,符合开闭原则.
缺点:
- 在使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值不同,大量的小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能.
- 装饰器模式提供了一种比继承更加灵活、机动的解决方案,但同时也意味着比继承更加易于出错,排错也更加困难,对于多次装饰的对象,在调试寻找错误时可能需要逐级排查,较为烦琐.
应用场景
-
快速动态扩展和撤销一个类的功能场景。 比如,有的场景下对 API 接口的安全性要求较高,那么就可以使用装饰模式对传输的字符串数据进行压缩或加密。如果安全性要求不高,则可以不使用。
-
不支持继承扩展类的场景。 比如,使用 final 关键字的类,或者系统中存在大量通过继承产生的子类。
4、适配器模式
4.1、定义
将类的接口转换为客户期望的另一个接口,适配器可以让不兼容的两个类一起协同工作。
例如USB转换器,将TypeC套在TypeA上就能用TypeC接口
4.2、主要角色
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:适配者即被适配的角色,它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
4.3、原理
4.3.1、类适配器
4.3.2、对象适配器
4.4、实现
4.4.1、类适配器
TypeAUSB(Target目标):适配的类
public class TypeAUSB {
public void loadDataToTypeA(){
System.out.println("使用TypeA接口加载数据");
}
}
TypeCUSB(Adatee适配者):被适配的角色
public interface TypeCUSB {
public void loadDataToTypeC();
}
TypeCAdapterTypeA(Adapter适配器):转换器
public class TypeCAdapterTypeA extends TypeAUSB implements TypeCUSB{
@Override
public void loadDataToTypeC() {
System.out.println("TypeCAdapter load TypeA USB!");
super.loadDataToTypeA();
}
}
4.4.2、类适配器测试代码
public class TestAdapter {
@Test
public void testExample03(){
TypeCAdapterTypeA adapterTypeA = new TypeCAdapterTypeA();
adapterTypeA.loadDataToTypeC();
}
}
4.4.3、类适配器测试效果
4.4.4、对象适配器
TypeAUSB(Target目标):适配的类
public class TypeAUSB {
public void loadDataToTypeA(){
System.out.println("使用TypeA接口加载数据");
}
}
TypeCUSB(Adatee适配者):被适配的角色
public interface TypeCUSB {
public void loadDataToTypeC();
}
TypeCAdapterTypeA(Adapter适配器):转换器
public class TypeCAdapterTypeA implements TypeCUSB{
private TypeAUSB typeAUSB;
public TypeCAdapterTypeA(TypeAUSB typeAUSB){
this.typeAUSB = typeAUSB;
}
@Override
public void loadDataToTypeC() {
System.out.println("TypeCAdapter load TypeA USB!");
typeAUSB.loadDataToTypeA();
}
}
4.4.5、对象适配器测试代码
public class TestAdapter {
@Test
public void testExample03(){
TypeCAdapterTypeA adapterTypeA = new TypeCAdapterTypeA(new TypeAUSB());
adapterTypeA.loadDataToTypeC();
}
}
4.4.6、对象适配器测试效果
4.5、总结
适配器模式优缺点
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无序修改原有结构
- 增加了类的透明性和复用性,将具体业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用.
- 灵活性和扩展性都非常好,通过使用配置文件可以很方便的更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,符合开闭原则.
缺点:
-
类适配器的缺点
- 对于Java等不支持多重继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 适配者类不能为最终类
-
对象适配器的缺点
- 与类适配器模式相比较,在该模式下要在适配器中置换适配者类的某些方法比较麻烦.
应用场景
-
统一多个类的接口设计时
某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义
-
需要依赖外部系统时
当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动
-
原有接口无法修改时或者原有接口功能太老旧但又需要兼容;
JDK1.0 Enumeration 到 Iterator 的替换,适用适配器模式保留 Enumeration 类,并将其实现替换为直接调用 Itertor.
-
适配不同数据格式时;
Slf4j 日志框架,定义打印日志的统一接口,提供针对不同日志框架的适配器
4.6、代理、桥接、装饰器、适配器 4 种设计模式的区别
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似,但其各自的用意却不同,简单说一下它们之间的关系:
-
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
-
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
-
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
-
适配器模式:将一个类的接口转换为客户希望的另一个接口。适配器模式让那些不兼容的类可以一起工作。
5、外观模式
5.1、定义
外观模式( Facade Pattern),也叫门面模式,外观模式的定义:为子系统中的一组接口提供统一的接口。它定义了一个更高级别的接口,使子系统更易于使用。
5.2、特点
外观模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
5.3、主要角色
-
外观(Facade)角色:为多个子系统对外提供一个共同的接口。
外观角色中可以知道多个相关的子系统中的功能和责任.在正常情况下,它将所有从客户端发来的请求委派到相应的子系统,传递给相应的子系统对象处理。
-
子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
每一个子系统可以是一个类也可以是多个类的集合。每一个子系统都可以被客户端直接调用,或者被外观角色调用,子系统并不知道外观的存在。对于子系统而言,外观角色仅仅是另一个客户端而已。
5.4、原理
5.5、实现
例如:智能音箱(SmartAppliancesFacade)控制电灯(Light)、电视(TV)、空调(AirCondition)
Light(SubSystemA):电灯
public class Light {
public void open(){
System.out.println("打开电灯……");
}
public void close(){
System.out.println("关闭电灯……");
}
}
TV(SubSystemB):电视
public class TV {
public void open(){
System.out.println("打开电视……");
}
public void close(){
System.out.println("关闭电视……");
}
}
AirCondition(SubSystemC):空调
public class AirCondition {
public void open(){
System.out.println("打开空调……");
}
public void close(){
System.out.println("关闭空调……");
}
}
SmartAppliancesFacade(Facade):智能音箱外观类
public class SmartAppliancesFacade {
private Light light;
private TV tv;
private AirCondition airCondition;
public SmartAppliancesFacade() {
this.light = new Light();
this.tv = new TV();
this.airCondition = new AirCondition();
}
public void say(String msg){
if(msg.contains("打开")){
open();
}else if(msg.contains("关闭")){
close();
}else{
System.out.println("对不起,没听清楚您说的是什么!请再说一遍!");
}
}
private void open() {
light.open();
tv.open();
airCondition.open();
}
private void close() {
light.close();
tv.close();
airCondition.close();
}
}
5.6、测试代码
public class TestFacade {
@Test
public void testExample02(){
// 创建外观模式
SmartAppliancesFacade facade = new SmartAppliancesFacade();
facade.say("打开家电");
facade.say("关闭家电");
}
}
5.7、测试效果
5.8、总结
外观模式优缺点
优点:
- 它对客户端屏蔽了子系统组件,减少了客户端所需要处理的对象数目,并使子系统使用起来更加的容易.通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少.
- 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
- 一个子系统的修改对其他子系统没有任何影响,而子系统内部变化也不会影响到外观对象.
缺点:
- 不能很好的控制客户端直接使用子系统类,如果客户端访问子系统类做太多的限制则减少了可变性和灵活性.
- 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则.
应用场景
- 简化复杂系统。 比如,当我们开发了一整套的电商系统后(包括订单、商品、支付、会员等系统),我们不能让用户依次使用这些系统后才能完成商品的购买,而是需要一个门户网站或手机 App 这样简化过的门面系统来提供在线的购物功能。
- 减少客户端处理的系统数量。 比如,在 Web 应用中,系统与系统之间的调用可能需要处理 Database 数据库、Model 业务对象等,其中使用 Database 对象就需要处理打开数据库、关闭连接等操作,然后转换为 Model 业务对象,实在是太麻烦了。如果能够创建一个数据库使用的门面(其实就是常说的 DAO 层),那么实现以上过程将变得容易很多。
- 让一个系统(或对象)为多个系统(或对象)工作。 比如,线程池 ThreadPool 就是一个门面模式,它为系统提供了统一的线程对象的创建、销毁、使用等。
- 联合更多的系统来扩展原有系统。 当我们的电商系统中需要一些新功能时,比如,人脸识别,我们可以不需要自行研发,而是购买别家公司的系统来提供服务,这时通过门面系统就能方便快速地进行扩展。
- 作为一个简洁的中间层。 门面模式还可以用来隐藏或者封装系统中的分层结构,同时作为一个简化的中间层来使用。比如,在秒杀、库存、钱包等场景中,我们需要共享有状态的数据时(如商品库存、账户里的钱),在不改变原有系统的前提下,通过一个中间的共享层(如将秒杀活动的商品库存总数统一放在 Redis 里),就能统一进行各种服务(如,秒杀详情页、商品详情页、购物车等)的调用。
6、组合模式
6.1、定义
将对象组合成树形结构以表示整个部分的层次结构。
(组合模式可以让用户统一对待单个对象和对象的组合)
6.2、特点
组合模式其实就是将一组对象(文件夹和文件)组织成树形结构,以表示一种’部分-整体’ 的层次结构,(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(文件夹)的处理逻辑(递归遍历)。
组合模式更像是一种数据结构和算法的抽象,其中数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。
6.3、主要角色
-
抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
在该角色中可以包含所有子类共有行为的声明和实现,在抽象根节点中定义了访问及管理它的子构件的方法,如增加子节点、删除子节点、获取子节点等。
-
树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
树枝节点可以包含树枝节点,也可以包含叶子节点,它其中有一个集合可以用于存储子节点,实现了在抽象根节点中定义的行为。包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
-
叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
在组合结构中叶子节点没有子节点,它实现了在抽象根节点中定义的行为。
6.4、原理
6.4、实现
Entry(Component):抽象类(文件夹 + 文件)
public abstract class Entry {
public abstract String getName(); // 获取文件名
public abstract int getSize(); // 获取文件大小
public abstract Entry add(Entry entry); // 添加文件或者文件夹
public abstract void printList(String prefix); // 显示指定目录下的所有文件信息
@Override
public String toString() {
return getName() + "(" + getSize() + ")";
}
}
Directory(Composite):表示文件夹
public class Directory extends Entry {
private String name;
private List<Entry> directory = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
/**
* 如果是 File,调用 getSize
* 如果是 Directory,会继续调用文件夹的getSize()方法,形成递归
*
* @return
*/
@Override
public int getSize() {
int size = 0;
for (Entry entry : directory) {
size += entry.getSize();
}
return size;
}
@Override
public Entry add(Entry entry) {
directory.add(entry);
return this;
}
@Override
public void printList(String prefix) {
System.out.println(prefix + "/" + this);
for (Entry entry : directory) {
entry.printList(prefix + "/" + name);
}
}
}
File(Leaf):文件类
public class File extends Entry {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getSize() {
return this.size;
}
@Override
public Entry add(Entry entry) {
return null;
}
@Override
public void printList(String prefix) {
System.out.println(prefix + "/" + this);
}
}
6.5、测试代码
public class TestComposite {
/**
* root
* - bin
* - vi
* - test
* - tmp
* - usr
* - mysql
* - my.conf
* - test.db
*/
@Test
public void testExample02() {
// 创建根节点
Directory root = new Directory("root");
// 创建叶子结点
Directory binDir = new Directory("bin");
binDir.add(new File("vi", 10000));
binDir.add(new File("test", 20022));
Directory tmp = new Directory("tmp");
Directory usrDir = new Directory("usr");
Directory mysqlDir = new Directory("mysql");
mysqlDir.add(new File("my.conf", 30));
mysqlDir.add(new File("test/db", 20));
usrDir.add(mysqlDir);
// 将所有子文件夹放到根节点
root.add(binDir);
root.add(tmp);
root.add(usrDir);
root.printList("");
}
}
6.6、测试效果
6.7、总结
1 ) 组合模式的分类
-
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
Component
声明了
add
、
remove
、
getChild
方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
- 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
2 ) 组合模式优点:
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
3 ) 组合模式的缺点:
- 使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。
4 ) 组合模式应用场景
-
处理一个树形结构,比如,公司人员组织架构、订单信息等;
-
跨越多个层次结构聚合数据,比如,统计文件夹下文件总数;
-
统一处理一个结构中的多个对象,比如,遍历文件夹下所有 XML 类型文件内容。
7、享元模式
7.1、定义
摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,从而让我们能在有限的内存容量中载入更多对象。
7.2、特点
节约内存空间,使用的办法是找出相似对象之间的共有特征,然后复用这些特征。
比如: 一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,我们可以使用享元模式解决这一类问题。
7.3、主要角色
-
抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
享元(Flyweight)模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
-
可共享的具体享元(Concrete Flyweight)角色:它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
-
非共享的具体享元(Unshared Flyweight)角色:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
-
享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
7.4、原理
7.5、实现
例如:建立五子棋工厂,生产白/黑子。
GoBangFactory(享元池):通过静态内部类实现GoBangFactory单例,生产五子棋棋子的工厂
public class GoBangFactory {
private Map<String, GoBang> pool = null;
public GoBangFactory() {
pool = new HashMap<>();
WhiteGoBang whiteGoBang = new WhiteGoBang();// 白子
BlackGoBang blackGoBang = new BlackGoBang();// 黑子
pool.put("w", whiteGoBang);
pool.put("b", blackGoBang);
}
private static class SingletonHandler{
private static final GoBangFactory INSTANCE = new GoBangFactory();
}
public static GoBangFactory getInstance(){
return SingletonHandler.INSTANCE;
}
public GoBang getGoBang(String key){
return pool.get(key);
}
}
GoBang(享元类):抽象五子棋类,定义主要功能或特征
public abstract class GoBang {
public abstract String getColor();
public void display(){// 显示棋子颜色
System.out.println("棋子的颜色:" + getColor());
}
}
WhiteGoBang(共享享元类):白色棋子
public class WhiteGoBang extends GoBang{
@Override
public String getColor() {
return "白色";
}
}
BlackGoBang(共享享元类):黑色棋子
public class BlackGoBang extends GoBang{
@Override
public String getColor() {
return "黑色";
}
}
7.6、测试代码
public class TestFlyweight {
@Test
public void testExample02() {
GoBangFactory instance = GoBangFactory.getInstance();
GoBang w1 = instance.getGoBang("w");
GoBang w2 = instance.getGoBang("w");
GoBang w3 = instance.getGoBang("w");
System.out.println("判断黑子是否是同一对象:" + (w1 == w2));
GoBang b1 = instance.getGoBang("b");
GoBang b2 = instance.getGoBang("b");
System.out.println("判断白子是否是同一对象:" + (b1 == b2));
b1.display();
b2.display();
w1.display();
w2.display();
w3.display();
}
}
7.7、测试效果
7.8、总结
1) 享元模式的优点
-
极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
比如,当大量商家的商品图片、固定文字(如商品介绍、商品属性)在不同的网页进行展示时,通常不需要重复创建对象,而是可以使用同一个对象,以避免重复存储而浪费内存空间。由于通过享元模式构建的对象是共享的,所以当程序在运行时不仅不用重复创建,还能减少程序与操作系统的 IO 交互次数,大大提升了读写性能。
-
享元模式中的外部状态相对独立,且不影响内部状态
2) 享元模式的缺点
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
3) 应用场景
-
一个系统有大量相同或者相似的对象,造成内存的大量耗费。
注意:在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
-
在 Java 中,享元模式一个常用的场景就是,使用数据类的包装类对象的 valueOf() 方法。比如,使用 Integer.valueOf() 方法时,实际的代码实现中有一个叫 IntegerCache 的静态类,它就是一直缓存了 -127 到 128 范围内的数值,如下代码所示,你可以在 Java JDK 中的 Integer 类的源码中找到这段代码。
public class Test1 {
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));
}
}
// 传入的值在-128 - 127 之间,直接从缓存中返回
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到
Integer
默认先创建并缓存
-128 ~ 127
之间数的
Integer
对象,当调用
valueOf
时如果参数在
-128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的
Integer
对象。
其实享元模式本质上就是找到对象的不可变特征,并缓存起来,当类似对象使用时从缓存中读取,以达到节省内存空间的目的。