设计模式-装饰器模式(java)
文章目录前言一、核心定义二、标准体系结构图三、场景推演四、实战案例4.1 需求分析4.2 架构图4.2.1 普通方式架构图4.2.2 装饰器模式架构图4.3 时序图4.3.1 普通代码时序图4.3.2 装饰器模式时序图4.5 代码分析4.5.1 普通代码if-else/硬编码4.5.2 装饰器模式代码总结前言装饰器模式Decorator Pattern属于结构型设计模式。它的核心思想是在不修改原有对象结构的前提下通过包装对象为原对象动态增加额外职责。它解决的不是“能不能加功能”的问题而是“如何在不破坏原有代码稳定性的前提下把新功能优雅地加上去”的问题。装饰器模式的典型认知误区遇到需要给某个已有类增加功能时第一反应总是继承——继承后重写方法把新逻辑写进去。这在功能单一、不再迭代的场景下没有问题但一旦需要横向扩展多种功能就会催生出大量子类最终维护成本失控。装饰器模式提供了另一条路不改变原有类不继承原有类而是用一个包装类在运行时动态叠加新功能。本文代码案例https://github.com/likerhood/CodeDesignWork/tree/main/codedesign8.0-0 和 8.0-1 、8.0-2一、核心定义装饰器模式也叫 Decorator、Wrapper是一种通过“组合”替代“继承”来扩展对象行为的模式。它的核心定义可以拆成三句话理解装饰器和被装饰对象实现同一个接口这样客户端使用时不需要关心拿到的是原始对象还是被包装后的增强对象。装饰器内部持有一个同接口对象的引用这个引用可以指向原始对象也可以指向另一个装饰器所以装饰器可以一层套一层。装饰器在调用原对象方法前后添加额外逻辑原有能力不变新增能力通过外层包装完成。一句话总结装饰器模式不是直接改被装饰对象 也不是简单继承它而是用一个新的对象包住它在不破坏原对象的情况下增强它。图片来源网址装饰设计装饰者模式 / 装饰器模式二、标准体系结构图«interface»Componentoperation()ConcreteComponentoperation()Decorator-component: Componentoperation()ConcreteDecoratoroperation()addedBehavior()标准结构说明角色作用Component定义原对象和装饰器共同遵守的接口ConcreteComponent被装饰的原始对象提供基础功能Decorator抽象装饰器内部保存一个Component引用ConcreteDecorator具体装饰器负责增加额外行为Client负责组装对象可以自由决定是否加装饰、加几层装饰关键点在于Decorator实现了Component同时又持有一个Component。这就形成了一个非常灵活的结构ComponentcomponentnewConcreteComponent();componentnewConcreteDecorator(component);客户端最终调用的还是component.operation()但实际执行时已经经过了装饰器增强。你熟悉的new BufferedReader(new FileReader())就是装饰器模式的经典体现一层嵌套一层字节流转字符流再转缓冲流原有对象完全不受影响。三、场景推演业务背景ERP系统使用SSO单点登录组件统一管理登录验证。初期系统只需验证登录 ticket 是否合法验证通过即可访问所有资源。演进随着团队扩大加入了运营、营销、数据等不同岗位人员需要对不同用户的访问方法范围做精细化管控——不是每个登录成功的用户都能访问所有接口。问题SSO是一个通用组件不能直接在里面添加特定业务的访问控制逻辑。需要在不修改 SSO 组件的前提下为其叠加方法访问范围校验功能。图片来源网址[重学 Java 设计模式实战装饰器模式「SSO单点登录功能扩展增加拦截用户访问方法范围场景」 | 小傅哥 bugstack 虫洞栈](https://bugstack.cn/md/develop/design-pattern/2020-06-09-重学 Java 设计模式《实战装饰器模式》.html) 补充说明什么是SSOSSOSingle Sign-On单点登录的作用是让用户只登录一次就可以访问多个相互信任的系统如公司内部的OA、ERP、CRM、财务、人事系统等。如果没有SSO访问每个系统都要单独登录有了SSO登录交由统一认证中心处理。用户登录成功后拿到凭证如 Ticket 或 Token之后访问其他业务系统只需携带该凭证即可。简而言之SSO解决的是“你是谁你有没有登录”但它不直接解决“你登录之后能访问什么你有没有权限调用某个方法”****两种解法对比解法做法代价继承重写LoginSsoDecorator extends SsoInterceptor在preHandle中重复SSO校验逻辑再追加权限逻辑SSO逻辑改了要同步改所有子类子类职责不清晰装饰器模式LoginSsoDecorator extends SsoDecorator(HandlerInterceptor)通过super.preHandle()委托 SSO 执行只关注权限扩展零代码冗余职责清晰新增扩展类型只加新装饰类四、实战案例4.1 需求分析系统原本已经有一个通用的SSO单点登录拦截器它只负责判断用户是否已经登录成功。最开始这完全没有问题因为需求很简单用户请求进入系统 ➔ 校验是否登录 ➔ 登录成功则放行失败则拦截随着业务变复杂不同用户访问ERP系统时不仅要判断“是否登录”还要判断“是否有权限访问某个方法”。新的业务流程变成了用户请求进入系统 ➔ SSO 登录校验 ➔ 登录成功后继续做方法权限校验 ➔ 均通过才放行痛点分析为什么不直接改面对新增的权限校验需求常规思路会带来明显的代码腐化如果直接修改SsoInterceptor会把业务权限逻辑侵入到通用登录组件中破坏它原本的稳定性。如果继承SsoInterceptor并重写方法看似扩展了功能但极易导致子类中重新复制一遍父类的登录校验逻辑再追加权限校验。一旦后续SSO校验规则发生变化多个子类都要跟着改维护成本极高。更合理的方式是保留原有SSO登录校验能力在它外面再包装一层权限校验能力。这正是装饰器模式发挥价值的地方。在本案例中各个角色的分工如下表所示角色分类对应类名职责说明统一接口HandlerInterceptor拦截器的顶级标准规范原始组件SsoInterceptor仅负责基础的SSO登录校验抽象装饰器SsoDecorator负责包装原始拦截器对象具体装饰器LoginSsoDecorator负责增加具体的方法权限校验逻辑最终客户端使用时可以这样优雅地组装new LoginSsoDecorator(new SsoInterceptor());这行代码的语义非常清晰用“权限校验装饰器”去包装“原始SSO登录拦截器”。实际执行流程为先执行SsoInterceptor的登录校验。登录成功后再执行LoginSsoDecorator的权限校验。4.2 架构图4.2.1 普通方式架构图使用的是继承方式。这个版本的问题是LoginSsoDecorator虽然继承了SsoInterceptor但它并没有复用父类的preHandle而是把父类里的登录校验逻辑又写了一遍。也就是说代码从SsoInterceptor 负责登录校验变成了LoginSsoDecorator 同时负责登录校验 权限校验短期看能跑长期看容易失控。如果后面继续增加“部门校验”“数据范围校验”“接口频率校验”就可能不断出现新的子类或者在一个类里堆越来越多逻辑。4.2.2 装饰器模式架构图核心变化是newLoginSsoDecorator(newSsoInterceptor())这句代码表达的意思是用 LoginSsoDecorator 包装 SsoInterceptor执行时先走装饰器booleansuccesssuper.preHandle(request,response,handler);而super.preHandle(...)实际会调用被包装对象handlerInterceptor.preHandle(request,response,handler);也就是LoginSsoDecorator - SsoDecorator - SsoInterceptor这样SsoInterceptor继续只做登录校验LoginSsoDecorator只在登录成功后增加权限校验。4.3 时序图4.3.1 普通代码时序图LoginSsoDecoratorApiTestLoginSsoDecoratorApiTestalt[登录失败][登录成功]preHandle(1successhuahua, response, handler)截取 ticket request.substring(1, 8)判断 ticket 是否等于 successfalse截取 userId request.substring(8)从 authMap 查询用户可访问方法判断 method 是否为 queryUserInfotrue / false这个版本中所有流程都在LoginSsoDecorator一个类里完成。问题是虽然类名叫LoginSsoDecorator但它并不是严格意义上的装饰器。 因为它没有包装一个SsoInterceptor对象而是通过继承重写了全部流程。4.3.2 装饰器模式时序图SsoInterceptorSsoDecoratorLoginSsoDecoratorApiTestSsoInterceptorSsoDecoratorLoginSsoDecoratorApiTestalt[SSO 登录失败][SSO 登录成功]preHandle(1successhuahua, response, handler)super.preHandle(request, response, handler)handlerInterceptor.preHandle(request, response, handler)截取 ticket 并判断是否为 successtrue / falsetrue / falsefalse截取 userId huahua查询 authMap 得到 queryUserInfo判断是否允许访问方法true / false它把两件事分开了类职责SsoInterceptor判断是否登录LoginSsoDecorator判断登录后是否有方法权限SsoDecorator连接原始对象和装饰对象4.5 代码分析4.5.1 普通代码if-else/硬编码tutorials-12.0-1的核心代码publicclassLoginSsoDecoratorextendsSsoInterceptor{privatestaticMapString,StringauthMapnewConcurrentHashMapString,String();static{authMap.put(huahua,queryUserInfo);authMap.put(doudou,queryUserInfo);}OverridepublicbooleanpreHandle(Stringrequest,Stringresponse,Objecthandler){Stringticketrequest.substring(1,8);booleansuccessticket.equals(success);if(!success)returnfalse;StringuserIdrequest.substring(8);StringmethodauthMap.get(userId);returnqueryUserInfo.equals(method);}}这个实现的优点是简单直接登录校验 - 权限校验 - 返回结果但缺点也非常明显重复了 SSO 登录校验逻辑SsoInterceptor已经有 ticket 校验但子类没有复用而是重新写了一遍。继承关系表达不准确LoginSsoDecorator extends SsoInterceptor看起来是扩展 SSO但实际是覆盖 SSO 的核心流程。扩展能力差如果继续增加更多校验比如角色、部门、数据权限、接口限流就会让这个类越来越膨胀。不利于运行时组合继承在编译期就确定了父子关系不能像装饰器一样灵活组装newADecorator(newBDecorator(newSsoInterceptor()))4.5.2 装饰器模式代码tutorials-12.0-2的核心代码分成两部分。第一部分是抽象装饰器SsoDecoratorpublicabstractclassSsoDecoratorimplementsHandlerInterceptor{privateHandlerInterceptorhandlerInterceptor;publicSsoDecorator(HandlerInterceptorhandlerInterceptor){this.handlerInterceptorhandlerInterceptor;}publicbooleanpreHandle(Stringrequest,Stringresponse,Objecthandler){returnhandlerInterceptor.preHandle(request,response,handler);}}这一层的重点不是业务逻辑而是结构设计。它通过构造函数接收一个HandlerInterceptorpublicSsoDecorator(HandlerInterceptorhandlerInterceptor)所以它可以包装newSsoInterceptor()也可以包装另一个装饰器newLoginSsoDecorator(newOtherDecorator(newSsoInterceptor()))第二部分是具体装饰器LoginSsoDecoratorpublicclassLoginSsoDecoratorextendsSsoDecorator{publicLoginSsoDecorator(HandlerInterceptorhandlerInterceptor){super(handlerInterceptor);}OverridepublicbooleanpreHandle(Stringrequest,Stringresponse,Objecthandler){booleansuccesssuper.preHandle(request,response,handler);if(!success)returnfalse;StringuserIdrequest.substring(8);StringmethodauthMap.get(userId);returnqueryUserInfo.equals(method);}}这段代码最关键的是booleansuccesssuper.preHandle(request,response,handler);它不是自己重新做登录校验而是把登录校验交回被包装的SsoInterceptor。只有登录成功后才执行自己的权限校验逻辑。测试代码LoginSsoDecoratorssoDecoratornewLoginSsoDecorator(newSsoInterceptor());Stringrequest1successhuahua;booleansuccessssoDecorator.preHandle(request,ewcdqwt40liuiu,t);这段组装关系非常重要LoginSsoDecorator 包装 SsoInterceptor最终含义是先执行 SSO 登录校验再执行登录后的权限校验总结装饰器模式解决的不是“能不能加功能”的问题而是“如何优雅地加功能”的问题。在本案例中tutorials-12.0-1通过继承完成了功能扩展但登录校验和权限校验耦合在一起还重复了父类逻辑tutorials-12.0-2使用装饰器模式后SsoInterceptor继续负责原始登录校验LoginSsoDecorator只负责新增权限校验职责更清楚扩展也更灵活。继承是“我变成你的一种特殊类型”装饰器是“我包住你并在你原有能力外面再加一层能力”。