结构型模式包括:适配器、桥接、组合、装饰器、外观、享元、代理

这类模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。

第四节、装饰器模式

一、装饰器模式介绍


装饰器的核心就是在不改变原有类的基础上给类新增功能。
不改变原有类,也可以使用继承、AOP切面。但是装饰器模式可以避免继承导致的子类过多,也可以避免AOP带来的复杂性。

很多场景都用到装饰器模式,比如学习java开发的字节流、字符流、文件流的内容时,一层嵌套一层的方式。

new BufferedReader(new FileReader(“”));

二、案例场景模拟


在本案例中我们模拟一个单点登录功能扩充的场景
⼀般在业务开发的初期,往往内部的ERP使⽤只需要判断账户验证即可,验证通过后即可访问ERP的所有资源。但随着业务的不断发展,团队⾥开始出现专⻔的运营⼈员、营销⼈员、数据⼈员,每个⼈员对于ERP的使⽤需求不同,有些需要创建活动,有些只是查看数据。同时为了保证数据的安全性,不会让每个⽤户都有最⾼的权限。

那么以往使⽤的 SSO 是⼀个组件化通⽤的服务,不能在⾥⾯添加需要的⽤户访问验证功能。这个时候我们就可以使⽤装饰器模式,扩充原有的单点登录服务。但同时也保证原有功能不受破坏,可以继续使⽤。

1.场景模拟工程

  1. 这里模拟的是spring中的类:HandlerInterceptor,实现接口SsoInterceptor模拟的单点登录拦截服务。
  2. 为了避免引入太多spring的内容影响对设计模式的阅读,这里使用了同名的类和方法,尽可能减少外部的依赖。

2.场景简述

2.1模拟spring的HandlerInterceptor
public interface HandlerInterceptor {
boolean preHandle(String request, String response, Object handler);
}

实际的单点登录开发会基于:org.springframework.web.servlet.HandlerInterceptor 实现。

2.2模拟单点登录功能
public class SsoInterceptor implements HandlerInterceptor{

@Override
public boolean preHandle(String request, String response, Object handler) {
String ticket = request.substring(1,8);
return ticket.equals("success");
}
}
  1. 这里的模拟实现非常简单只是截取字符串,实际使用需要从 HttpServletRequest request 对象中获取 cookie 信息,解析 ticket 值做校验。
  2. 在返回的里面也非常简单,只要获取到了 success 就认为是允许登录。

三、继承类实现

此场景大多数实现的方式都会采用继承类,继承类的实现方式也是一个比较通用的方式,通过继承后重新给方法,并发将自己的逻辑覆盖过去。

1.工程结构


以上工程结构非常简单,只是通过LoginSsoDecorator继承SsoInterceptor,重写方法功能。

2.代码实现

public class LoginSsoDecorator extends SsoInterceptor{
private static Map<String, String> authMap = new ConcurrentHashMap<>();
static {
authMap.put("levtio", "queryUserInfo");
authMap.put("juis", "queryUserInfo");
}
@Override
public boolean preHandle(String request, String response, Object handler) {
// 模拟获取cookie
String ticket = request.substring(1,8);
// 模拟校验
boolean success = ticket.equals("success");
if (!success) {
return false;
}
String userId = request.substring(8);
String method = authMap.get(userId);

// 模拟方法校验
return "queryUserInfo".equals(method);
}
}

以上部分通过继承重写方法,将个人可访问哪些方法的功能添加到方法中。

3.测试验证

3.1编写测试类
public class ApiTest {
@Test
public void test_LoginSsoDecorator() {
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator();
String request = "1successlevtio";
boolean success = ssoDecorator.preHandle(request, "loginsuccess", "t");
System.out.println("登录校验:" + request + (success ? " 放行" : " 拦截"));
}
}

这里模拟的相当于登录过程中的校验操作,判断用户是否可登录以及是否可访问方法。

3.2测试结果
登录校验:1successlevtio 放行

四、装饰器模式重构代码

接下来使用装饰器模式来进行代码优化,也算一次很小的重构。

装饰器主要解决的是直接继承下因功能的不断横向扩展导致子类膨胀的问题,而是用装饰器模式后就会比直接继承显得更加灵活同时这样也就不再需要考虑子类的维护。

在装饰器模式中有四个比较重要的抽象出来的点:

  1. 抽象构件角色(Component)- 定义抽象接口
  2. 具体构件角色(ConcreteComponent)- 实现抽象接口,可以是一组
  3. 装饰角色(Decorator)- 定义抽象类并继承接口中的方法,保证一致性
  4. 具体装饰角色(ConcreteDecorator)- 扩展装饰具体的实现逻辑
    通过以上这四项来实现装饰器模式,主要核心内容会体现在抽象类的定义和实现上

1.工程结构

装饰器模式模型结构

  1. 以上是一个装饰器实现的类图结构,重点的类是SsoDecorator,这个类是一个抽象类主要完成了对接口HandlerInterceptor继承。
  2. 当装饰角色继承接口后会提供构造函数,入参就是继承的接口实现类即可,这样就可以很方便的扩展出不同功能组件。

2.代码实现

2.1抽象类装饰角色
public class SsoDecorator implements HandlerInterceptor{

private HandlerInterceptor handlerInterceptor;
private SsoDecorator(){}
public SsoDecorator(HandlerInterceptor handlerInterceptor){
this.handlerInterceptor = handlerInterceptor;
}
@Override
public boolean preHandle(String request, String response, Object handler) {
return handlerInterceptor.preHandle(request,response,handler);
}
}
  1. 在装饰类中有三个重点的地方,1)继承了处理接⼝、2)提供了构造函数、3)覆盖了⽅法preHandle。
  2. 以上三个点是装饰器模式的核心处理部分,这样可以踢掉对子类继承的方式实现逻辑功能扩展。
2.2装饰角色逻辑实现
public class LoginSsoDecorator extends SsoDecorator{
private Logger logger = LoggerFactory.getLogger(LoginSsoDecorator.class);
private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();

static {
authMap.put("levtio", "queryUserInfo");
authMap.put("juis", "queryUserInfo");
}

public LoginSsoDecorator(HandlerInterceptor handlerInterceptor) {
super(handlerInterceptor);
}

@Override
public boolean preHandle(String request, String response, Object handler) {
boolean success = super.preHandle(request, response, handler);
if (!success) {
return false;
}
String userId = request.substring(8);
String method = authMap.get(userId);
logger.info("模拟单点登录方法访问拦截校验:{} {}", userId, method);
// 模拟方法校验
return "queryUserInfo".equals(method);
}
}
  1. 在具体的装饰类实现中,继承了装饰类 SsoDecorator ,那么现在就可以扩展⽅法: preHandle
  2. 在preHandle 的实现中可以看到,这⾥只关⼼扩展部分的功能,同时不会影响原有类的核⼼服务,也不会因为使⽤继承⽅式⽽导致的多余⼦类,增加了整体的灵活性。

3.测试验证

3.1编写测试类
public class ApiTest {
@Test
public void test_LoginSsoDecorator() {
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator(new SsoInterceptor());
String request = "1successlevtio";
boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
System.out.println("登录校验:" + request + (success ? " 放行" : " 拦截"));
}
}
  1. 这里测试了对装饰器模式的使用,通过透传原有单点登录类new SsoInterceptor(),传递给装饰器,让装饰器可以执行扩充的功能。
  2. 同时对于传递者和装饰器都可以是多组的,在⼀些实际的业务开发中,往往也是由于太多类型的⼦类实现⽽导致不易于维护,从⽽使⽤装饰器模式替代。
3.2测试结果
16:41:26.991 [main] INFO  o.l.demo.design.LoginSsoDecorator - 模拟单点登录方法访问拦截校验:levtio queryUserInfo
登录校验:1successlevtio 放行

还有⼀种场景也可以使⽤装饰器。例如;你之前使⽤某个实现某个接⼝接收单个消息,但由于外部的升级变为发送 list 集合消息,但你⼜不希望所有的代码类都去修改这部分逻辑。那么可以使⽤装饰器模式进⾏适配 list 集合,给使⽤者依然是 for 循环后的单个消息

五、总结

  1. 使⽤装饰器模式满⾜单⼀职责原则,你可以在⾃⼰的装饰类中完成功能逻辑的扩展,⽽不影响主类,同时可以按需在运⾏时添加和删除这部分逻辑。另外装饰器模式与继承⽗类᯿写⽅法,在某些时候需要按需选择,并不⼀定某⼀个就是最好。
  2. 装饰器实现的᯿点是对抽象类继承接⼝⽅式的使⽤,同时设定被继承的接⼝可以通过构造函数传递其实现类,由此增加扩展性并᯿写⽅法⾥可以实现此部分⽗类实现的功能。
  3. 就像夏天热你穿短裤,冬天冷你穿棉裤,⾬天挨浇你穿⾬⾐⼀样,你的根本本身没有被改变,⽽你的需求却被不同的装饰⽽实现。⽣活中往往⽐⽐皆是设计,当你可以融合这部分活灵活现的例⼦到代码实现中,往往会创造出更加优雅的实现⽅式。