来源|阿里开发者公众号
作者|幽霄
一、背景
前台业务同学在业务承接过程中总是抱怨大部分业务无法通过设计模式来承接,写的代码是越来越没有追求,理由是我无法预测未来的业务的发展,且设计模式更多的是在框架或中间件中使用。然而设计模式是对能力抽象出的通用模式,从哲学的角度来看世间万物皆尘土,事物都是可以抽象出共同的本质的东西。所以,难道只有底层能力可以抽象,业务逻辑部分就不可以抽象了?必须可以才是啊。在前台业务承接过程中除了能力可以抽象,还有可以抽象出业务流程,假设在有这样一些业务场景,品搜和图搜、直播间评论和点赞、公域直播会场和私域商详透直播等等,这些各领域内的业务流程“大同小异”,因此都可以抽象出通用的业务流程节点。但是通常在一个主干流程需要承接的场景有很多,比如直播间互动这个主干流程包括了直播间评论、点赞、求讲解、看证书、进场等等场景,所以我们需要通过主要流程进行进行多场景承接。但是这样还不够,在面对多端型多场景的情况下需要处理返回不同的数据模型。综上所述,我们如何通过一个主干业务流程承接多个业务场景并在数据上可适配到多端型多场景,实现在服务端高质量高效率的“包接口”,下面会详细介绍。
二、业务承接
如果你面临的问题是在同一个业务域上承接多种类似的业务场景,每天在适配各种端型或者各种场景而对外提供接口时,为了保证应用系统的可扩展性和高效承接业务,那么可以按照如下步骤进行设计。
2.1 业务流程抽象
首先需要进行业务建模,抽象出用户用例或者user story,当然具体的粒度可以自己把控,具体如下:
2.1.1 用户用例
在直播间互动领域内的用户用例如下:
从整个系统出发,挖掘出面向不同的用户提供的能力有哪些,在这些用例背后需要进行的流程和节点又是什么。通过这些流程和节点才能进行后续的系统时序和流程抽象,举例如下
在互动领域内的流程和节点如下:
- 风控检查
- 评论持久化
- 评论上屏
- 评论进沟通
2.2.2 系统时序
基于用户用例进行分析,这些用例都需要经过什么的流程节点进行处理,然后将这些流程按照系统时序进行呈现。
到此基于上述的用例和时序是不是可以抽象出具体互动流程了,显而易见。
2.2.3 业务流程抽象
有了系统用例和系统时序这一步就比较简单,从系统时序里很容易可以抽象出具体的流程和流程中的处理节点,具体如下:
- 对于直播间互动领域可以抽象出的业务流程:风控检查->互动内容持久化-》消息上屏-》互动进IM
- 对于直播间分发领域可以抽象出的业务流程:直播推荐-》直播间基础信息-》直播流信息-》直播间品信息
到此,大家可以按照上述步骤在大脑里对自己的业务域进行抽象出来了。
2.2.4 设计模式固化主流程
按照业务主流程可以通过模板模式将处理流程固定下来,如下所示:
@Override @LiveLog(logResult = true) public InteractionResult interactionSubmit(MobileInteractionRequest request, InteractionLiveRoom liveRoom) { Boolean needSave = MapUtils.getBoolean(request.getExtInfo(), LiveInteractionConstant.NEED_SAVE); // 默认保存 InteractionResult saveResult = null; if (Objects.isNull(request.getExtInfo()) || Objects.isNull(needSave) || needSave) { saveResult = save(request, liveRoom); if(Objects.nonNull(saveResult) && !saveResult.isSuccess()) { return saveResult; } } // 默认进沟通 InteractionResult chatResult; if (Objects.isNull(request.getSendToChat()) || Boolean.parseBoolean(request.getSendToChat())) { chatResult = sendToChat(request); if(Objects.nonNull(chatResult) && !chatResult.isSuccess()) { return chatResult; } } if(Objects.nonNull(saveResult) && saveResult.isSuccess()) { return saveResult; } return null; } /** * 互动行为保存到数据库或者缓存中 * * @param request * @return */ protected abstract InteractionResult save(MobileInteractionRequest request, InteractionLiveRoom liveRoom); /** * 进沟通 * * @param request * @return */ protected abstract InteractionResult sendToChat(MobileInteractionRequest request);
2.2 业务流程扩展
因在上述模版模式中预留了两个扩展点,所以在子类中可以通过扩展点进行扩展,举例如下:
如果有更多的场景就需要扩展实现上述两个扩展点进行扩展即可,这样保证了业务的高效承接。这里会有两个问题:
- 在程序运行时如何根据具体的场景选择哪个子类进行逻辑处理
- 如何进行适配端和场景返回的数据模型
针对第一个问题,其实就是如何去if else的问题,这里也给出比较经典的方案:
- 枚举法
- 表驱动法
- 策略模式+工厂模式
其中枚举法和表驱动法比较简单易用,原理就是将映射关系封装在枚举类或本地缓存中,这里简单介绍下如何通过策略模式消除if else。
// 策略接口 public interface Opt { int apply(int a, int b); } // 策略实现类 @Component(value = "addOpt") public class AddOpt implements Opt { @Autowired xxxAddResource resource; // 这里通过Spring框架注入了资源 @Override public int apply(int a, int b) { return resource.process(a, b); } } // 策略实现类 @Component(value = "devideOpt") public class devideOpt implements Opt { @Autowired xxxDivResource resource; // 这里通过Spring框架注入了资源 @Override public int apply(int a, int b) { return resource.process(a, b); } } // 策略处理 @Component public class OptStrategyContext{ private Map<String, Opt> strategyMap = new ConcurrentHashMap<>(); @Autowired public OptStrategyContext(Map<String, TalkService> strategyMap) { this.strategyMap.clear(); this.strategyMap.putAll(strategyMap); } public int apply(Sting opt, int a, int b) { return strategyMap.get(opt).apply(a, b); } }
总结伪代码:
// 抽象类固定业务流程 预留扩展点 public abstract class AbstractXxxx { doXxx(Object context) { // 节点1 doNode1(context); // 节点2 doNode2(context); // 节点3 doNode3(context); // 节点n ... } // 扩展点1 protected abstract Result doNode1(Object context); // 扩展点2 protected abstract Result doNode2(Object context); // 扩展点3 protected abstract Result doNode3(Object context); } // 策略处理 public class OptStrategyContext{ private Map<String, Opt> strategyMap = new ConcurrentHashMap<>(); static { // 上述模版模式的实现类 strategyMap.put("business1", Xxxx1); strategyMap.put("business2", Xxxx2); strategyMap.put("business3", Xxxx3); } // 初始化 public OptStrategyContext(Map<String, Opt> strategyMap) { this.strategyMap.clear(); this.strategyMap.putAll(strategyMap); } public int doXxxx(Object context) { return strategyMap.get(business).doXxxx(context); } }
2.3 多场景多端型适配
上面我们只是通过模版模式抽象出了主干业务流程,但是如何适配不同的端型和不同的场景,返回不同的数据模型呢,这里有两种答案,一种是模版模式、另一种是“棒棒糖”模式,下面逐一介绍。
2.3.1 模版模式适配
既然是模版模式,这里的主干流程又是什么呢?主要跟我们解决的问题有关系,按照2.1中的流程步骤,可以抽象出固定的流程为:请求入参处理-》业务逻辑处理-》结果返回处理。
其中业务逻辑处理可以选定为2.2中介绍的通过策略模式选择业务扩展的子类,来处里业务部分;请求入参和结果返回处理部分可以设置为扩展点,供子类扩展。具体伪代码如下:
// 抽象类固定业务流程 预留扩展点 适配多端型多场景 public abstract class AbstractSceneAdapter { <T> T doXxx(Object context) { // 节点1 doRequestFilter(context); // 节点2 getBusinessService(context).doBusiness(context); // 节点3 return doResultWrap(context); } // 扩展点1 protected abstract Result doRequestFilter(Object context); // 扩展点2 protected abstract Result doBusiness(Object context); // 扩展点3 protected abstract Result doResultWrap(Object context); // 业务逻辑处理子类 protected abstract BusinessService getBusinessService(Object context); } // 策略处理 根据不同端型场景选择合适的子类 public class SceneAdapterViewService { private Map<String, SceneAdapter> strategyMap = new ConcurrentHashMap<>(); static { // 上述模版模式的实现类 strategyMap.put("scene1", Xxxx1); strategyMap.put("scene2", Xxxx2); strategyMap.put("scene3", Xxxx3); } // 初始化 public SceneAdapterViewService(Map<String, SceneAdapter> strategyMap) { this.strategyMap.clear(); this.strategyMap.putAll(strategyMap); } public Result doXxxx(Object context) { return strategyMap.get(scene).doXxxx(context); } }
注:因要适配不同端型不同场景返回不同的数据模型,所以上述伪代码中主流程最终返回的结果是一个泛型,在子类实现的时候进行确定具体返回的类型。
2.3.1 棒棒糖模式适配
通过模版模式来适配时会有一个小问题,当需要有多个请求入参处理器或者多个结果包装器的时候需要在模版里增加处理节点,但其实这些节点是有共性的可抽象出来的。因此可以针对入参处理器和结果包装器定义单独的接口,需要多个处理器时同时实现接口进行处理。然后这些实现类打包放在单独的类中依次执行即可。当然其中的业务处理部分也可以定义接口动态实现。伪代码如下:
// 入参处理器 public interface IRequestFilter<> { void doFilter(T t); } // 结果包装器 public interface IResultWrapper<R, T> { Result<R> doWrap(Result<T> res); } public class SceneAdapterViewService implements InitializingBean { private List<IRequestFilter> filters; private List<IResultWrapper> wrappers; private Map<String, SceneAdapter> strategyMap = new ConcurrentHashMap<>(); // 请求过滤器实现类 @Autowired @Qualifier("filter1") private IRequestFilter filter1; @Autowired @Qualifier("filter2") private IRequestFilter filter2; // 结果处理器实现类 @Autowired @Qualifier("wrapper1") private IResultWrapper wrapper1; // 业务处理实现类 @Autowired @Qualifier("scene1") private SceneAdapter scene1; @Autowired @Qualifier("scene2") private SceneAdapter scene2; @Autowired @Qualifier("scene3") private SceneAdapter scene3; // 主方法 publice Result sceneAdapte(Object context) { // 请求入参过滤 异常时返回 for(int i = 0; i<filters.size(); i++) { try { filters.get(i).doFilter(context) } catch { return null; } } // 策略模式执行业务逻辑,执行是按照模版模式 Result res = strategyMap.get(scene).doXxxx(context); Result result = res; // 过滤处理,包括树结构改变,数据字段裁剪等 for(int i = 0; i<wrappers.size(); i++) { try { result = wrappers.get(i).doWrap(result) } catch { return res; } } return result; } // 初始化各个节点值 @Override public void afterPropertiesSet() throws Exception { // 入参过滤器 可多个 filters.add(filter1); filters.add(filter2); // 结果处理器 可多个 wrappers.add(wrapper1); // 业务处理部分 strategyMap.put("scene1", Xxxx1); strategyMap.put("scene2", Xxxx2); strategyMap.put("scene3", Xxxx3); } }
三、接口设计
基于上述两种设计模式来适配时我们的接口又该如何设计,是设计面向通用的业务层接口还是面向定制化的业务接口,两种方式各有优缺点:
优点 |
缺点 |
|
通用业务接口 |
1,扩展性强 2,业务承接效率高,无需频繁发布代码 |
1,业务边界不清晰,问题排查效率低 2,接口数据冗余 |
定制化业务接口 |
1,业务边界清晰 2,接口出入参数据无冗余 |
1,无扩展 2,频繁包接口发布代码 |
对于接口提供者来说肯定不希望频繁改动代码发布代码,但是又希望能够在业务承接过程中能够高效适配多端型多场景,因此这里总结了下接口设计原则:
1、对于越底层的接口应该越通用,例如HSF接口、领域服务、中间件提供的接口;
2、对于越上层的接口应该越定制化,例如对于不同的UI适配、不同的场景适配等;
3、对于业务领域内的接口应该通用化,例如直播业务域的分发领域、互动领域内的接口尽可能的通用化;
四、总结
在承接业务过程中会面临频繁包接口、一个view层的数据模型充满了小100个属性,系统的扩展性遇到瓶颈,这些问题除了通过平台化配置化的能力来解决,但是回归到代码本身我们任然可以通过抽象的设计模式来解决。
- 基于抽象的理论达到复用、高内聚低耦合,降低系统复杂度的目标,设计模式不只是用在底层能力或中间件中,在业务承接过程中亦有大的功效。
- 千万不要为了用设计模式而刻意使用设计模式,带来的效果适得其反,在选择设计模式时也要三思,落地后再改动成本将会巨大。
- 在前台业务开发中,需要划分主各个业务领域,在领域中抽象出该业务的处理流程,基于流程可设计相关的扩展和编排能力,方式有很多种,包括SPI、设计模式、DSL等,本文主要通过模版模式和棒棒糖模式来解决问题。
- 接口设计应该按照越底层越通用,越上层越定制化的原则进行设计,当然在业务域内的接口应尽可能的通用话。