valuechain模型案例,基于CommonsChain类库重构电商营销活动中的会员资格校验模块
valuechain模型案例,基于CommonsChain类库重构电商营销活动中的会员资格校验模块(题外话,Struts,Hibernate框架为什么会没落,先哲老子在《道德经》有言,“反者道之动,弱者道之用。”,事物发展到极致,就会走向其反面。Struts把MVC做到了极致,Hibernate把ORM做到了极致,结果二者分别被轻量级的后辈SpringMVC和Mybatis,青出于蓝而胜于蓝了。众所周知,知名的开源框架,往往是有灵魂的,是为了某个模式(在这里,说“设计模式”可能不太严谨)而生的。早期著名的SSH(Spring Struts Hibernate)企业级开发架构,里面的每个框架都有其灵魂,Spring以DI或者IOC为魂,Struts以MVC为魂,Hibernate以ORM为魂,目前比较热门的SSM(Spring、SpringMVC、MyBatis)企业级开发架构,SpringMVC虽然后来居上,替代了Struts,但是仍然以MVC为魂,MyBatis类似Hibernate
Apache的Commons Chain开源类库,以这个设计模式为灵魂,专为这个设计模式的落地而生;而且它还得到了,Web开发框架中昔日的霸主——Struts1的认可,协助Struts1完成了一个很重要的功能模块。
大家好,欢迎关注极客架构师,极客架构师——专注架构师成长,我是码农老吴。
本节是《架构师基本功之设计模式》第12期-第2节
在本期第1节,我给大家分享了行为型设计模式中的责任链模式(chain of Responsibility pattern),基于责任链模式,重构了电商营销活动中,常见的会员资格校验功能,完成了两版代码。
在本节,我将给大家介绍,基于责任链模式的,apache开源类库——Commons Chain,并使用它来重新实现,会员资格校验模块,这是第三版代码。
我们一起感受一下,这个轻量级的开源类库,是如何帮助我们,或者其他的开源框架(如Struts1),落地责任链设计模式的,如何简化我们使用责任链模式的一些繁琐工作,专注业务本身。
当然,这个开源类库,也随着Struts1的落幕,而光环退却。它的最后一次发布,已定格在了2012年2月14日。不过,在github上,它还是有一批拥趸在坚守。仅就责任链模式而言,它的功能已经非常完善,只不过随着时代的发展,它里面的某些组件,在当时看很先进的理念,现在已经有些落伍了。
模式的荣耀众所周知,知名的开源框架,往往是有灵魂的,是为了某个模式(在这里,说“设计模式”可能不太严谨)而生的。早期著名的SSH(Spring Struts Hibernate)企业级开发架构,里面的每个框架都有其灵魂,Spring以DI或者IOC为魂,Struts以MVC为魂,Hibernate以ORM为魂,目前比较热门的SSM(Spring、SpringMVC、MyBatis)企业级开发架构,SpringMVC虽然后来居上,替代了Struts,但是仍然以MVC为魂,MyBatis类似Hibernate,也以ORM为魂,只不过没有Hibernate那么轴,把ORM做的那么决绝。
(题外话,Struts,Hibernate框架为什么会没落,先哲老子在《道德经》有言,“反者道之动,弱者道之用。”,事物发展到极致,就会走向其反面。Struts把MVC做到了极致,Hibernate把ORM做到了极致,结果二者分别被轻量级的后辈SpringMVC和Mybatis,青出于蓝而胜于蓝了。
究其原因,就是过于追求极致,导致系统复杂,复杂导致难以驾驭,最终被码农抛弃了。码农的世界里,只有一个主宰,那就是码农自己。码农对于框架或者类库,不仅仅知其然,更喜欢知其所以然。而如果一个系统太过复杂,则知其所以然的成本就非常高,上手也不太容易,让码农丧失了掌控感。)
MVC,DI,ORM这些模式,也因这些开源框架的盛行,而风生水起,荣耀万分。不过,在GOF的23中设计模式中,能有自己开源框架(或者类库)的,为数不多,责任链模式,就是其中一个。而以责任链设计模式为魂的,就是apache的开源类库——Commons Chain。
Apache Commons Chain 类库为责任链模式而生Apache Commons chain开源类库的开发者,在其官网首页,开宗明义地阐述了这个开源类库的价值。
A popular technique for organizing the execution of complex processing flows is the "Chain of Responsibility" pattern as described (among many other places) in the classic "Gang of Four" design patterns book.
正如像“Gang of Four”在其设计模式书中(以及其他很多地方)描述的那样,责任链模式是一种流行的,用于组织和执行复杂处理流程的技术。
Although the fundamental API contracts required to implement this design pattern are extremely simple it is useful to have a base API that facilitates using the pattern and (more importantly) encouraging composition of command implementations from multiple diverse sources.
尽管执行责任链模式的基本API规范往往很简单,但是建立一个基础的API来简化和辅助这个模式的使用,还是很有用的,并且(更重要的是)鼓励组合来自多个不同来源的命令实现。
为了尊重作者,上面的翻译,我用的是直译,可能有些地方不太好懂,特别是最后一句,“鼓励组合来自多个不同来源的命令实现”,这句话我是这样理解的,可能不太精确,就是这个开源类库,在落地责任链模式时,同时使用了另外一个设计模式,就是命令模式(Command Pattern),而责任链模式中,最接近命令模式的,就是请求处理者角色。随着后面的讲解,大家会进一步有所了解。
这个类库,重要组件如下所示。
- Context:用于封装请求参数和处理结果,充当请求角色和处理结果角色。
- Command:基于命令模式,实现请求处理者角色。
- Chain:顾名思义,就是可以包含多个请求处理者的链,相当于链管理者角色。只不过要注意的是,它同时也是Command对象,可以包含到其他Chain中,也就是链是可以嵌套的。
- Filter:是一种特殊类型的Command,多了一个postprocess()方法,用于在Chain执行完毕后,释放命令占用的相关资源。
- Catalog:它有点务虚,主要是给Command或者Chain组件,提供一个逻辑名称,用于类名称的解耦合(它已经被时代淘汰,Spring框架就可以轻松代劳)。
我们结合下面的案例,来了解这几个组件,但是我们的案例中,并没有涉及到Filter和Catelog组件,大家如果想了解,可以娄一眼官网Cookbook上的案例,那个案例是一个测试驱动开发(TDD,Test Driven Development)的经典案例。
第三版-基于Commons-chain开源类库类图接口及类:
ActivityControllerV3:controller
IVerificationServiceV3:service接口
VerificationServiceImplV3:service实现类,请求发送者角色
ActivityContext:context组件,请求角色,处理结果角色
ActivityCommandChainA:Chain组件,链管理者角色
IsPlusCommand:command组件,请求处理者角色
HasBoughtCommand:command组件,请求处理者角色
HasFollowedShopCommand:command组件,请求处理者角色
交互图 ActivityControllerV3:controller和前面两版没啥区别。
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo02.Activity;
import com.geekarchitect.patterns.cor.demo02.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
/**
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
@Controller
public class ActivityControllerV3 {
private static final Logger LOG = LoggerFactory.getLogger(ActivityControllerV3.class);
@Autowired
private IVerificationServiceV3 verificationServiceV3;
public String verification(Long memberId Long activityId) {
LOG.info("Controller层");
Member member = new Member();
member.setId(memberId);
Activity activity = new Activity();
activity.setId(activityId);
Boolean result = verificationServiceV3.isLegalMember(member activity);
LOG.info("会员{} 活动{} 是否有资格参与该活动{}" memberId activityId result);
return "";
}
}
IVerificationServiceV3:service接口
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo02.Activity;
import com.geekarchitect.patterns.cor.demo02.Member;
/**
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
public interface IVerificationServiceV3 {
/**
* 是否有资格参加活动
*
* @param member
* @param activity
* @return
*/
Boolean isLegalMember(Member member Activity activity);
}
VerificationServiceImplV3:service实现类,请求发送者角色
作为责任链模式中的请求发送者角色,这个类要注意以下内容:
1,封装参数,建立请求对象,使用的是ActivityContext类。
2,接受处理结果,是从ActivityContext的对象中获取,不是依靠execute()的返回值。
3,它依赖的对象,是ActivityCommandChainA(链管理者角色)。
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo02.Activity;
import com.geekarchitect.patterns.cor.demo02.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
@Service
public class VerificationServiceImplV3 implements IVerificationServiceV3 {
private static final Logger LOG = LoggerFactory.getLogger(VerificationServiceImplV3.class);
@Autowired
private ActivityCommandChainA activityCommandChainA;
@Override
public Boolean isLegalMember(Member member Activity activity) {
LOG.info("Service层");
//封装请求对象
ActivityContext activityContext = new ActivityContext();
activityContext.setMember(member);
activityContext.setActivity(activity);
boolean result = false;
try {
activityCommandChainA.execute(activityContext);
} catch (Exception e) {
LOG.info("异常信息 {}" e);
}
//获取处理结果
result = activityContext.isResult();
LOG.info("result={}" result);
return result;
}
}
ActivityContext:context组件,请求角色,处理结果角色
这个类,做了两件事情,既封装了请求参数,又封装了处理结果,所以,在责任链模式里面,它属于请求角色和处理结果角色。
那为什么我们不自己建立一个实体类,封装这些信息呢,当然可以,但是Commons Chain中的Context组件提供了一个功能更为强大的参数封装的类。
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo02.Activity;
import com.geekarchitect.patterns.cor.demo02.Member;
import lombok.Data;
import org.apache.commons.chain.impl.ContextBase;
/**
* 请求角色:
* 既封装请求参数,又封装处理结果
*
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
@Data
public class ActivityContext extends ContextBase {
public static String MEMBER_KEY="member";
private Member member;
private Activity activity;
//处理结果
private boolean result;
}
扩展知识:Context组件
这个类库的Context组件,给我们提供了一个兼容Map-style风格和JavaBean风格,并且数据类型安全的参数封装类。
Map-Stype风格:设置参数调用put(key value)方法,获取参数调用get(key)方法
JavaBean风格:设置参数调用相应属性的setProperty()方法,获取属性则调用相应的getProperty()方法。
以上两种风格,在Context组件的对象中是可以互操作的。
下面我编写了一个测试用例,展示这几种特性。
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo02.Activity;
import com.geekarchitect.patterns.cor.demo02.Member;
import com.geekarchitect.patterns.cor.demo03.ActivityControllerV2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.*;
class ActivityContextTest {
private static final Logger LOG = LoggerFactory.getLogger(ActivityContextTest.class);
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}
@Test
void testMapStyle() {
ActivityContext activityContext=new ActivityContext();
Member member=new Member();
member.setId(1L);
activityContext.put(ActivityContext.MEMBER_KEY member);
Member result1=(Member)activityContext.get(ActivityContext.MEMBER_KEY);
LOG.info("result1={}" result1.toString());
Member result2=activityContext.getMember();
LOG.info("result2={}" result2.toString());
}
@Test
void testJavaBeanStyle() {
ActivityContext activityContext=new ActivityContext();
Member member=new Member();
member.setId(1L);
activityContext.setMember(member);
Member result1=(Member)activityContext.get(ActivityContext.MEMBER_KEY);
LOG.info("result1={}" result1.toString());
Member result2=activityContext.getMember();
LOG.info("result2={}" result2.toString());
}
@Test
void testTypeStyle() {
ActivityContext activityContext=new ActivityContext();
Activity activity=new Activity();
activity.setId(2L);
activityContext.put(ActivityContext.MEMBER_KEY activity);
Member result1=(Member)activityContext.get(ActivityContext.MEMBER_KEY);
LOG.info("result1={}" result1.toString());
Member result2=activityContext.getMember();
LOG.info("result2={}" result2.toString());
}
}
运行结果
ActivityCommandChainA:Chain组件,链管理者角色这个类,从责任链模式看,属于链管理者角色,也就意味着,它需要把多个请求处理者链接起来,形成一个链,以便请求发送者,发送的请求对象,可以在链中传递。
它是通过addCommand()方法,来完成这个任务的。
从类库的角度看,它属于Chain组件。这里我们要扩展一下。
package com.geekarchitect.patterns.cor.demo04;
import org.apache.commons.chain.impl.ChainBase;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* chain组件
* 链管理者角色
*
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
@Component
public class ActivityCommandChainA extends ChainBase implements InitializingBean {
@Autowired
private IsPlusCommand isPlusCommand;
@Autowired
private HasFollowedShopCommand hasFollowedShopCommand;
@Autowired
private HasBoughtCommand hasBoughtCommand;
public ActivityCommandChainA() {
}
@Override
public void afterPropertiesSet() throws Exception {
//将请求处理者角色加入链中
addCommand(isPlusCommand);
addCommand(hasFollowedShopCommand);
addCommand(hasBoughtCommand);
}
}
扩展知识:Chain组件
类图:
从类图中我们可以看到,Chain组件最上面的父接口,是Command接口,所以,Chain组件同时也是Command组件,这也就意味中,Chain组件是可以嵌套的。一个Chain可以作为一个Command,添加到另外一个Chain里面。
IsPlusCommand:command组件,请求处理者角色这个类,从责任链模式角度看,属于请求处理者角色,从命令模式角度看,属于命令角色,从当前类库看,属于类库里面的Command组件。
对于这个类,要特别特别注意的,是它里面的execute方法,我们看下面的扩展知识点。
package com.geekarchitect.patterns.cor.demo04;
import com.geekarchitect.patterns.cor.demo03.IsPlusHandler;
import org.apache.commons.chain.Command;
import org.apache.commons.chain.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Random;
/**
* command组件
* 请求处理者角色
*
* @author 极客架构师@吴念
* @createTime 2022/9/16
*/
@Component
public class IsPlusCommand implements Command {
private static final Logger LOG = LoggerFactory.getLogger(IsPlusHandler.class);
@Override
public boolean execute(Context context) throws Exception {
LOG.info("IsPlusCommand");
boolean isPlus = new Random().nextBoolean();
boolean result = CONTINUE_PROCESSING;
if (!isPlus) {
result=PROCESSING_COMPLETE;
LOG.info("非plus会员,校验终止 isPlus={}" isPlus);
} else {
LOG.info("plus会员,继续校验 isPlus={}" isPlus);
}
return result;
}
}
扩展知识:Command组件
Command接口
这个组件,要特别关注的,就是execute方法的返回值,是一个boolean类型的值,它的作用是表示是否继续执行后面的节点。返回false表示继续把请求传递给它后面的节点,反之,则终止当前所在的Chain组件。那么如果我们需要返回其他内容时,就需要使用Context组件了。为了方便,这个接口还专门定义了两个boolean常量,方便我们使用。
运行结果 案例点评总结一下,到底Apache的Commons Chain类库,在责任链设计模式中,到底帮助我们做了哪些事情呢。
1,Context组件:为我们提供了map-style风格和JavaBean风格的参数封装,并且支持一定的type-safe。
2,Chain组件:这个组件,应该是功能最大的,帮助我们建立请求处理者的链,并且负责在链中传递请求对象。
3,Command组件:虽然只提供了一个接口,但是,它基于Command pattern模式,给我们提供了能配合Chain组件,完成责任链模式的请求处理者。
虽然这个类库,在官网的Cookbook页面,花大量篇幅,介绍了如何在Struts框架中使用责任链模式,但是内容实在是有些过时了,感兴趣或者工作中有需要的朋友可以自己看看。
责任链模式需要深入思考的点 是否只能被一个请求处理者处理答案是否定的,虽然在责任链模式的原始定义里面,确实说明了,只需要被一个对象处理即可,但是在实际的应用时,即可以只被一个处理者处理,也可以被多个处理者处理,具体怎么做,就看业务场景的需要,不用拘泥于原始的定义。
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
——Gof《设计模式:可复用面向对象软件的基础》
为什么要增加请求角色和处理结果角色是为了强调,在实际开发中,由于有多个请求处理者,那么如果给这些处理者传递参数,处理结果怎么能适应所有的处理者,都需要认真考虑,所以,把它们定义为专门的角色,更方便设计。在Apache的Commons Chain类库中,是通过Context组件,来同时封装请求数据和处理结果的,也是一个很好的设计方案。
为什么要增加链管理者角色在原著中,对链是如何创建的,何时创建的,没有专门的说明,我分析原因可能是,作者列举的案例,是通过,请求处理者是通过继承关系来实现对象链的,所以是一种天然的创建链的方式。而在实际的业务中,如何把多个请求处理者链起来,情况很多,不同的业务差异性很大,往往需要架构师花费不少的精力,来设计方案,所以独立起来更好。在Apache的Commons Chain类库中,是通过Chain组件来充当链管理者角色的。
链是否只能是线性的,是否可以有分支,甚至循环一般是线性的,比较简单。但是,也可以由请求处理者,根据具体情况,判断下一步应该执行哪个节点,就可以产生分支了,甚至有可能导致循环处理。但是如果做的太复杂,责任链模式就有些力不从心了,使用专业的工作流框架,可能会更好一些。
通用代码和类图类图 序列图 后续规划有关责任链模式的主要内容,我们就分享到这里,后面我还会在《源码说》系列讲座中,分享责任链模式是如何在别的开源软件中落地的,这是后话。
具体到下一期,我计划给大家分享的是,有点偏门的,有点难度的解释器模式,为了讲解这个设计模式,我想玩个大的,专门设计一个自己的“编程语言”,来配合讲解解释器模式,需要准备的时间可能稍微长一些,大家敬请期待(万一搞不出来,码农老吴丢人就丢出银河系啦)。
极客架构师,专注架构师成长。
关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。