设计模式之装饰器模式
来源自《重学Java设计模式》链接提取码:ytc3
装饰器模式
初看上图感觉装饰器模式有点像俄罗斯套娃、汽车组装,而装饰器的核心就是在不改变原有类的基础上给类新增功能。不改变原有类,可能你会想到继承、AOP切面,当然这些方式都可以实现,但是使用装饰器模式会是另外一种思路,更为灵活,可以避免继承导致的子类过多,也可以避免AOP带来的复杂性。
使用到装饰器模式的场景
new BufferedReader(new FileReader())
;,这段代码你是否熟悉,在java开发到字节流、字符流、文件流的内容时都见到了这样的代码,一层嵌套一层,字符流转字节流等等,而这样方式的使用就是装饰器模式的一种体现。
案例场景
在本案例中我们模拟了一个单点登录功能扩充的场景
一般在业务开发的初期,往往内部的ERP使用只需要判断账户验证即可,验证通过后即可访问ERP的所有资源。但随着业务的不断发展,团队里开始出现专门的运营人员、营销人员、数据人员,每个人员对于ERP的使用需求不同,有些需要创建活动,有些只是查看数据。同时为了保证数据的安全性,不会让每个用户都有最高的权限。
那么以往使用的sso
是一个组件化通用的服务,不能在里面添加需要的用户访问验证功能。这个时候我们就可以使用装饰器模式,扩充原有的单点登录服务。但同时也保证原有功能不受破坏,可以继续使用。
场景模拟
- 这里模拟的是Spring中的类:
HandlerInterceptor
,实现接口功能SsoInterceptor
模拟的单点登录拦截服务。
场景简述
-
模拟Spring的HandlerInterceptor
public interface HandlerInterceptor { boolean preHandle(String request, String response, Object handler); }
-
模拟单点登录功能
public class SsoInterceptor implements HandlerInterceptor{ public boolean preHandle(String request, String response, Object handler) { // 模拟获取cookie String ticket = request.substring(1, 8); // 模拟校验 return ticket.equals("success"); } }
- 这里的模拟实现非常简单只是截取字符串,实际使用需要从
request
对象中获取cookie
信息,解析ticket
值做校验。 - 关于其中返回的信息,只要获取到了
success
就认为是允许登录。
- 这里的模拟实现非常简单只是截取字符串,实际使用需要从
常规实现
此场景大多数实现的方式都会采用继承类
继承类的实现方式也是一种比较通用的方式,通过继承后重写方法,并将自己的逻辑覆盖进去。如果是一些简单的场景且不需要不断维护和扩展的,此类实现并不会有什么,也不会导致子类过多。
通过
LoginSsoDecorator
继承SsoInterceptor
,重写方法功能。
代码实现
public class LoginSsoDecorator extends SsoInterceptor {
private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();
static {
authMap.put("huahua", "queryUserInfo");
authMap.put("doudou", "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(9);
String method = authMap.get(userId);
// 模拟方法校验
return "queryUserInfo".equals(method);
}
}
- 以上这部分通过继承重写,将个人可访问哪些方法的功能添加到方法中。
测试验证
@Test
public void test_LoginSsoDecorator() {
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator();
String request = "1successhuahua";
boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
System.out.println("登录校验:" + request + (success ? " 放行" : " 拦截"));
}
- 这里模拟的相当于登录过程中的校验操作,判断用户是否可以登录以及是否可以访问方法。
result
登录校验:1successhuahua 拦截
Process finished with exit code 0
- 从结果来看满足我们的预期,做了拦截。
使用装饰器模式
装饰器主要解决的是直接继承下因功能的不断横向扩展导致子类膨胀的问题,而是用装饰器模式后就会比直接继承显得更加灵活同时这样也就不再需要考虑子类的维护。
在装饰器模式中有四个比较重要要抽象出来的点:
- 抽象构件角色(Component)-
定义抽象接口
- 具体构件角色(ConcreteComponent)-
实现抽象接口,可以是一组
- 装饰角色(Decorator)-
定义抽象类并继承接口中的方法,保证一致性
- 具体装饰角色(ConcreteDecorator)-扩展装饰具体的实现逻辑
通过以上这四项来实现装饰器模式,主要核心内容会体现在抽象类的定义和实现上。
装饰器模式结构模型
- 以上是一个装饰器实现的类图结构,重点的类是
SsoDecorator
,这个类是一个抽象类主要完成了对接口HandlerInterceptor
继承 - 当装饰角色继承接口后会提供构造函数,入参就是继承的接口实现类即可,这样就可以很方便的扩展出不同的功能的组件。
代码实现
-
抽象类装饰角色
public abstract class SsoDecorator implements HandlerInterceptor { private HandlerInterceptor handlerInterceptor; private SsoDecorator(){} public SsoDecorator(HandlerInterceptor handlerInterceptor) { this.handlerInterceptor = handlerInterceptor; } public boolean preHandle(String request, String response, Object handler) { return handlerInterceptor.preHandle(request, response, handler); } }
- 在装饰类中有几个重点的地方是:(1)继承了处理接口、(2)提供了构造函数、(3)覆盖了方法
preHandle
。 - 以上三个点是装饰器模式的核心处理部分,这样可以剃掉对子类继承的方式实现逻辑功能扩展。
- 在装饰类中有几个重点的地方是:(1)继承了处理接口、(2)提供了构造函数、(3)覆盖了方法
-
装饰角色逻辑实现
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("huahua", "queryUserInfo"); authMap.put("doudou", "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); } }
- 在具体的装饰类实现中,继承了装饰类
SsoDecaorator
,那么现在就可以扩展方法:preHandle
- 在
preHandle
的实现中可以看到,这里这关心扩展部分的功能,同时不会影响原有类的核心服务,也不会因为使用继承方式而导致的多余子类,增加了整体的灵活性。
- 在具体的装饰类实现中,继承了装饰类
测试验证
public class ApiTest {
@Test
public void test_LoginSsoDecorator() {
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator(new SsoInterceptor());
String request = "1successhuahua";
boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
System.out.println("登录校验:" + request + (success ? " 放行" : " 拦截"));
}
}
- 这里测试了对装饰器模式的使用,通过原有单点登录类
new SsoInterceptor()
,传递给装饰器,让装饰器可以执行扩充的功能。 - 同时对于传递者和装饰器都可以是多组的,在一些实际的业务开发中,往往也是由于太多类型的子类实现而导致不易于维护,从而使用装饰器模式替代。
result
14:41:02.777 [main] INFO o.i.demo.design.LoginSsoDecorator - 模拟单点登录方法访问拦截校验:huahua queryUserInfo
登录校验:1successhuahua 放行
Process finished with exit code 0
- 结果符合预期,扩展了对方法拦截的校验性
- 另外,还有一种场景也可以使用装饰器模式,例如:你之前使用某个实现某个接口接收单个消息,但由于外部的升级变为发送
list
集合消息,但你又不希望所有的代码类都去修改这部分逻辑。那么可以使用装饰器模式进行适配list
集合,给使用者依然是for
循环后的单个消息。
总结
- 使用装饰器模式满足单一职责原则,你可以在自己的装饰类中成功完成逻辑的扩展,而不影响主类,同时可以按需在运行时添加和删除这部分逻辑。另外装饰器模式与继承父类重写方法,在某些时候需要按需选择,不一定某一个就是最好。
- 装饰器实现的重点是对抽象类继承接口的使用,同时设定被继承的接口可以通过构造函数传递器实现类,由此增加扩展性并重写方法里可以实现此部分父类实现的功能。
评论区