做不好业务中台,你可能是缺了这种设计

文章目录

  • 一、前言
    • 1 什么是中台
    • 2 为什么需要业务中台
  • 二、扩展点机制
    • 2.1 青铜菜鸟: If一把梭
    • 2.2 荣耀黄金: 策略模式消除If
    • 2.3 荣耀王者: 扩展点机制
  • 三、源码示范
    • 3.1 模型
      • a. 3个扩展点数据模型
      • b. 2个注解
      • c. 1个扩展点管理类
    • 3.2 运行机制
      • a. 扩展点扫描&注册
      • b. 扩展点执行
    • 3.3 小结
    • 3.4 Spring注解开发
  • 四、总结

一、前言

1 什么是中台

中台这个概念近几年很火,互联网公司聊起技术架构言必谈中台,好像聊架构不说到中台建设就显得很Low。中台到底是什么?目前网上比较主流的中台定义和分类有如下三种:

  • 技术中台: 如微服务框架、Devops平台、容器之类
  • 业务中台: 指微服务业务平台,像常见交易中台、订单中心、营销中心
  • 组织中台: 偏向战略或组织架构层面,类似企业内部的资源调度和指挥系统

2 为什么需要业务中台

技术中台和组织中台,不是我们关心的,抛开这两个我们聚焦到业务中台。先看看为什么需要业务中台,由于楼主近两年的工作与交易相关项目做得比较多,这里面的痛点也比较深,故这里以交易中台举例来看看为什么需要业务中台。

  • 没有中台之前
    在交易中台出现之前,各垂直业务线各自为战,自顾自投入人力搞一套交易系统,然后对接底层各种公共服务,比如商户中心、商品中心、订单中心、支付中心等等。这里不得不吐槽下,底层能有这些公共服务已经算技术架构做得很不错了。在楼主的工作经历中,这些公共服务最开始是严重缺失的,都是伴随着业务发展,逐步投入人力建设才有的。服务接入好不好用,说多了都是泪。吐槽完毕,从下图中显而易见:1、站在各垂直业务线的角度,每个业务线都需要投入人力去了解、对接各种公共服务,并维护业务侧交易系统;2、站在底层公共服务的角度,每个业务线来对接,都需花时间人肉答疑、配合业务侧接入测试联调,这样公共服务技术团队的人效就很低了,况且做的对接又没什么技术含量。3、站在企业角度,貌似投入了很多HC,但做的事情却是重复建设,成本上去了效率没上去。
    做不好业务中台,你可能是缺了这种设计_第1张图片

  • 有了中台之后
    看看上图中那么多根线,就知道有多乱了,这还仅仅是以一个业务交易系统建设为例,如果每个业务线再单独搞搞营销系统、商品系统,这画面就更加没法看!面向对象设计思想告诉我们,没有什么是加一层抽象不能解决的。有了交易中台这一层,我们看看发生了什么变化?1、各垂直业务线的交易系统,从原来对接N多个公共服务,变成只需对接交易中台一个服务,并且交易逻辑复用交易中台能力就只是简单的Api调用,工作量大大减少;2、底层公共服务也得到解放,从原来要直接服务好N个业务线系统,转变成只服务好交易中台这一个客户,对接一次一劳永逸;3、交易中台团队的任务也比较标准化,向上做好垂直业务一键接入,向下做好公共服务对接。随着后续垂直业务线接入的增多,中台模式的降本增效会越来越凸显,这也是企业喜闻乐见的。
    做不好业务中台,你可能是缺了这种设计_第2张图片

PS: 有没有觉得,中台模式就是设计模式里面的中介者模式(Mediator Pattern)


二、扩展点机制

前一章,花了较多篇幅对比阐明中台模式的魅力。理想很丰满,现实很骨感。每个垂直业务线之所以能独立存在,肯定有与其他业务线不一样的业务形态,否则所有业务线合成一个不就OK了。业务系统既然是为业务定制的,那这些定制的业务逻辑和业务规则怎么办?直观想法是,逻辑下层到交易中台。逻辑下层是没错,但怎么下沉就比较有讲究了。这也是本文核心要论述到的中台系统设计必备之扩展点机制。

2.1 青铜菜鸟: If一把梭

逻辑下层简单,直接在代码里面用if判断,下面以订单优惠计算伪代码示例:

public int calcPromotion() {
    // 通用优惠计算处理流程
    ...
    // 业务定制逻辑处理
    if(bizLine = A) {
      // A业务附加操作
    }
    if(bizLine = B) {
      // B业务附加操作
    }
    ...
    if(bizLine = X) {
      // X业务附加操作
    }
    ...
}

这种作法的缺点很明显,通用逻辑和业务定制逻辑混在一起,充斥着各种业务定制If,又长有臭。而且If还不能一次写到头,每新增一个业务线,If就会增加一条,一个优惠计算服务每次都得改代码。严重违反对扩展开放对修改关闭的设计原则,我相信,如果中台系统以这种方式实现肯定会被喷成翔。

2.2 荣耀黄金: 策略模式消除If

业务逻辑下沉,上一个方案是通过各种If判断来实现的,这种方式很生硬也不符合开闭原则。熟悉设计模式的技术老鸟,应该很容易想到用策略模式去化解显示的If判断。给个示范:

  • 各业务线优惠计算策略类
@Service
class APromotionCaclulate implements PromotionCaclulateStrategy  {
    
    @Override
    public int calcPromotion() {
        // 通用优惠计算处理流程
        ...
        // A业务定制逻辑处理
        ...
    }

    @Override
    public String bizId(){
        return "A"
    }
}

@Service
class BPromotionCaclulate implements PromotionCaclulateStrategy {
    
    @Override
    public int calcPromotion() {
        // 通用优惠计算处理流程
        ...
        // B业务定制逻辑处理
        ...
    }

    @Override
    public String bizId(){
        return "B"
    }
} 
  • 中台优惠计算服务
@Service
public class DefaultPromotionCaclService {

    private final Map<String, PromotionCaclulateStrategy> strategyMap = Maps.newConcurrentMap();

    @Autowired
    public DefaultPromotionCaclService(List<PromotionCaclulateStrategy> strategyList) {
        strategyMap.clear();
        strategyList.forEach(strategy -> strategyMap.putIfAbsent(strategy.bizId(), strategy));
    }

    public int calcPromotion() {
        // 从请求上下文获得业务身份
        String bizId = "A";

        // 加载业务线优惠计算策略
        PromotionCaclulateStrategy strategy = strategyMap.get(bizId);

        // 执行优惠计算
        return strategy.calcPromotion();
    }
}

通过策略模式 ,我们完全消灭了If分支判断,各业务线定制逻辑也收敛到单独的策略类里面,逻辑上更内聚。同时,如果有新业务线接入 ,只需新建一个策略类,中台的优惠计算服务不用改一行代码,完全符合开闭原则。

其实,做到这一步已经不错了。每个类职责单一,逻辑也内聚,也具备扩展性。但对于中台系统来说 ,还做得不够。具体而言缺陷有两点:1、每个策略类里面,都有部分通用的优惠计算代码,为什么这部分通用逻辑要重复写N遍!2、中台通用业务流程的Java文件和业务定制的策略类Java文件混在在一起,二者是1:N的关系(N代表接入业务线个数),类文件膨胀很快。不易分清主次,也不方便做好业务线隔离,每新接入一条业务线,整个中台系统还得重新发版部署。

2.3 荣耀王者: 扩展点机制

针对上面策略模式提到的两点问题,有没有办法解决呢?当然有,下面就轮到本文真正的主角出场:扩展点机制!其主要思想为:

  • 业务中台重心放在自身业务处理流程编排上。所谓业务处理流程,应该是比较固定通用的。比如,交易中台的下单服务其固定流程大致为: 查询买家、查询商户、查询商品,然后进行优惠计算、价格计算、下单前置检查、创建不可见订单、扣库存、订单设置为可见。

  • 业务处理流程应该是极少变化的,业务中台在每个业务处理流程中,对事先考虑到会变化的部分预留扩展点,用来给具体业务线来定制实现。

  • 业务调用中台服务时,携带业务身份。中台服务根据业务身份加载定制的扩展点实现类,完成业务定制逻辑处理。

结合下图,具体阐述下:

  • 首先,交易中台编写业务处理流程时,适度留下一些扩展点给业务定制。比如下单接口,留下两个扩展点:优惠计算、下单前置校验;
  • 其次,交易中台对各业务线编写插件包,并以二方包的形式引入工程。当交易中台系统启动的时候,就会扫描到这些扩展包,并完成扩展点注册;
  • 最后,当收到业务系统的下单请求(请求必须携带业务身份)时,交易中台走固定的buy业务流程,遇到扩展点,则去调用对应业务线扩展点,最终走完整个流程。

一言以蔽之,通用处理流程代码在中台系统;业务定制逻辑代码在业务插件扩展包,通过扩展点注册、查找、执行机制完成通用逻辑和业务定制逻辑的隔离和整合。做不好业务中台,你可能是缺了这种设计_第3张图片


三、源码示范

图看懂了,原理也知道了,这套扩展点机制具体怎么落地,相信才是大家最关心的。楼主习惯以码服人,已贴心地准备好源码。这套扩展点机制核心代码<200行,你没听错,200行不能再多了,就是这么简洁。

3.1 模型

a. 3个扩展点数据模型

  • 扩展点坐标: 可简单类比Maven坐标,注册、查找扩展点会用到
/**
 * 扩展点坐标: 注意查找注入扩展点是根据 hash(bizCode, extensionPoint) 来定位
 */
@EqualsAndHashCode(of = {"bizCode", "extensionPoint"})
@Builder
@Getter
@Setter
@ToString
public class ExtensionCoordinate {

    /** 类比Maven的groupId */
    private String bizCode;

    /** 类比Maven的artifactId */
    private String extensionPoint;

    private String name;

    private String description;

    public static ExtensionCoordinate of(@NonNull String bizCode, @NonNull String extensionPoint) {
        return ExtensionCoordinate.builder()
                                  .bizCode(bizCode)
                                  .extensionPoint(extensionPoint)
                                  .build();
    }

}
  • 扩展点标记接口: 为什么需要一个标记接口,原因在于进行扩展包扫描的时候,需要识别出哪些Bean是扩展点实例,故需要一个标记接口,凡是实现了扩展点标记接口的Bean,系统就知道这是一个扩展点实例,需要对其进行扩展点注册。
/**
 * 没有什么方法, 仅用作扩展点标记接口
 */
public interface IExtensionPoint {

    MidFrameworkException NO_ROUTER_EXCEPTION = new MidFrameworkException("No router");

    /** 扩展点接口命名规范: 扩展点接口名必须以ExtPt结尾 */
    String EXT_POINT_NAMING = "ExtPoint";

    /** 平台实现的扩展点实例注册统一用 $SYSTEM$ 这个bizCode */
    String SYSTEM_BIZ_CODE = "$SYSTEM$";

}
  • 业务身份标识: 业务身份用来辅助查找定位扩展点实例,扩展点坐标的bizCode就是它!
/**
 * 业务身份
 */
@Value
public class BizId {

    private String bizCode;

}

b. 2个注解

需要注意到: 1、@Extension 组合了 @Componnet 注解,故扩展点实现类带上 @Extension 就能被Spring的Bean扫描机制加载到; 2、@SystemImpl 组合了 @Extension 注解

@Component
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Extension {

    String bizCode() default "";

    String name() default "";

    String description() default "";

}

/**
 * 系统实现的各业务通用扩展点
 */
@Extension
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SystemImpl {

    String bizCode() default IExtensionPoint.SYSTEM_BIZ_CODE;

    String name() default "";

    String description() default "";

}

c. 1个扩展点管理类

  • 扩展点管理类: 单例对象,用来存放扩展点实例,并提供扩展点注册、查找接口
/**
 * 扩展点管理类
 */
public final class ExtensionManager {

    private static ExtensionManager instance = new ExtensionManager();

    private static Map<ExtensionCoordinate, IExtensionPoint> extensionHub = new ConcurrentHashMap<>(64);

    public static ExtensionManager getInstance() {
        return instance;
    }

    /**
     * 注册扩展点实例
     *
     * @param coord     扩展点实例坐标
     * @param extension 扩展点实例对象
     * @return 之前注册的扩展点实例; 如果不为null, 表示发生了重复注册!
     */
    public IExtensionPoint registerExtPoint(ExtensionCoordinate coord, IExtensionPoint extension) {
        return extensionHub.put(coord, extension);
    }

    /**
     * 查找扩展点实例
     */
    public IExtensionPoint findExtPoint(ExtensionCoordinate coord) {
        return extensionHub.get(coord);
    }

    /**
     * 全部扩展点实例; 可用户扩展点可视化管理
     */
    public Map<ExtensionCoordinate, IExtensionPoint> listAllExtPoint() {
        return Collections.unmodifiableMap(extensionHub);
    }

}

3.2 运行机制

有了以上数据模型,我们再看看这些数据模型是如何联动起来发挥作用的。

a. 扩展点扫描&注册

要想扩展点能Work,第一步当然是要完成扩展点注册,怎么做到的呢?这里借助的是Spring的扩展机制BeanPostProcessor,当IoC容器启动时: 1、逐个Bean检查是否为扩展点实例(通过检查是否为IExtensionPoint实例来判断);2、如果是,则解析 @Extension 注解(注: @SystemImpl 因为组合有 @Extension,故也能被解析到), 进而构建出扩展点坐标 ExtensionCoordinate;3、直接调用扩展点管理类ExtensionManager注册方法,完成扩展点注册。

  • 扩展点扫描
/**
 * 扩展点扫描、注册
 */
public class ExtPointBeanPostProcess implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    /**
     * 识别扩展点实例, 并完成扩展点注册
     * 1.扩展点必须实现IExtensionPoint接口
     * 2.强制要求扩展点接口名必须以ExtPt结尾
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof IExtensionPoint) {
            IExtensionPoint extPoint = (IExtensionPoint) bean;

            ExtensionCoordinate coordinate = parseExtensionCoordinate(bean.getClass());
            IExtensionPoint oldValue = ExtensionManager.getInstance()
                                                       .registerExtPoint(coordinate, extPoint);
            if (Objects.nonNull(oldValue)) {
                throw new MidFrameworkException("Duplicate registration for :" + coordinate);
            }
        }
        return bean;
    }

    private ExtensionCoordinate parseExtensionCoordinate(Class<?> targetBean) {
        // 扩展点接口
        Optional<Class> extPointClass = checkExtensionPoint(targetBean);
        if (!extPointClass.isPresent()) {
            throw new MidFrameworkException(
                    String.format("The name of ExtensionPoint for %s must be end with %s", targetBean, IExtensionPoint.EXT_POINT_NAMING));
        }

        // 解析@Extension注解
        AnnotationAttributes attrs = AnnotatedElementUtils.getMergedAnnotationAttributes(targetBean, Extension.class);
        String bizCode = attrs.getString("bizCode");
        String name = attrs.getString("name");
        String description = attrs.getString("description");

        return ExtensionCoordinate.builder()
                                  .bizCode(bizCode)
                                  .extensionPoint(extPointClass.get().getName())
                                  .name(name)
                                  .description(description)
                                  .build();
    }

    private Optional<Class> checkExtensionPoint(Class<?> targetBean) {
        Class[] interfaces = targetBean.getInterfaces();
        if (interfaces == null || interfaces.length == 0) {
            throw new MidFrameworkException("No extension point interface for " + targetBean);
        }

        // 扩展点接口名必须以ExtPt结尾
        return Arrays.stream(interfaces)
                     .filter(ext -> ext.getSimpleName().endsWith(IExtensionPoint.EXT_POINT_NAMING))
                     .findFirst();
    }

}

b. 扩展点执行

经过以上步骤,系统已经启动完成,并且所有的扩展点也都完成了注册。剩下的就是怎么去调用扩展点方法了!由于同一个扩展点的所有实例可能是同一个名字,比如都叫
BuyExtPointImpl,显然不能通过Spring的依赖注入来调用。事实上,扩展点实例的调用,都是统一走扩展点执行器,其内部封装了扩展点查找逻辑和执行逻辑,代码注释应该写得比较清楚了。

/**
 * 扩展点实例执行器: 封装扩展点查找策略及方法调用
 */
public class ExtensionExecutor  {

    private ExtensionExecutor() {
        throw new IllegalStateException("No instance");
    }

    /**
     * 有返回值
     *
     * @param targetClass 扩展点类名
     * @param bizId       业务身份
     * @param exeFunction 需要调用的扩展点方法
     */
    public static <R, T> R execute(BizId bizId, Class<T> targetClass, Function<T, R> exeFunction) {
        T component = findComponent(bizId, targetClass);
        return exeFunction.apply(component);
    }

    /**
     * 无返回值
     *
     * @param targetClass 扩展点类名
     * @param bizId       业务身份
     * @param exeConsumer 需要调用的扩展点方法
     */
    public static <T> void consume(BizId bizId, Class<T> targetClass, Consumer<T> exeConsumer) {
        T component = findComponent(bizId, targetClass);
        exeConsumer.accept(component);
    }

    /**
     * 精确查找扩展点: 先看是否有业务实现的扩展点;若没有,再看是否有平台默认实现
     */
    @SuppressWarnings("unchecked")
    private static <T> T findComponent(BizId bizId, Class<T> targetClass) {
        ExtensionCoordinate bizCoordinate = ExtensionCoordinate.of(bizId.getBizCode(), targetClass.getName());
        IExtensionPoint extension = ExtensionManager.getInstance().findExtPoint(bizCoordinate);
        if (Objects.isNull(extension)) {
            ExtensionCoordinate systemCoordinate = ExtensionCoordinate.of(IExtensionPoint.SYSTEM_BIZ_CODE,
                    targetClass.getName());
            extension = ExtensionManager.getInstance().findExtPoint(systemCoordinate);
        }

        if (Objects.isNull(extension)) {
            throw new MidFrameworkException("Not found extension: " + bizCoordinate);
        }
        return (T) extension;
    }

}

下面以一个单测为例演示扩展点的执行 ExtensionExecutor.execute()

@Test
public void testTaobaoExt() {
    BizId bizId = new BizId("com.taobao");
    int promotion = ExtensionExecutor.execute(bizId, PromotionCalcExtPoint.class,
            PromotionCalcExtPoint::calcPromotion);
    Assert.assertEquals(promotion, 80);
}

@Test
public void testTmallExt() {
    BizId bizId = new BizId("com.tmall");
    int promotion = ExtensionExecutor.execute(bizId, PromotionCalcExtPoint.class,
            PromotionCalcExtPoint::calcPromotion);
    Assert.assertEquals(promotion, 90);
}

3.3 小结

上面2小节,已经把扩展点注册和扩展点执行,涉及到的数据模型、运行机制阐述清楚了。这里再补一幅图来个更直观感受。
做不好业务中台,你可能是缺了这种设计_第4张图片

  • 扩展点注册

    • 通过扩展点扫描, 加载所有实现扩展点标记接口的bean(Step1);
    • 通过解析Bean的@Extension 注解,构建出扩展点坐标(Step2);
    • 调用扩展点管理类的注册方法,完成扩展点注册(Step3)
  • 扩展点执行

    • 通过业务请求携带的业务身份和扩展点接口信息,构建扩展点坐标(Step4);
    • 调用扩展点管理类的查找方法,找到扩展点实例(Step5);
    • 通过扩展点执行器触发扩展点方法(Step6)

3.4 Spring注解开发

楼主的想法把这个扩展点机制做成基础组件,由各中台系统以SDK方式引入使用。Spring的牛叉大家都是知道的,为了方便开箱即用,楼主顺带也搞了个注解启用开关。使用方式是,工程引入SDK后,如果想使用扩展点机制,在启动类上加上 @EnableExtension 即可。

/**
 * 启用扩展点扫描
 */
@Import(ExtensionAutoConfiguration.class)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface EnableExtension {

}

public class ExtensionAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(ExtensionAutoConfiguration.Marker.class)
    public Marker marker() {
        return new ExtensionAutoConfiguration.Marker();
    }

    @Bean
    @ConditionalOnBean(ExtensionAutoConfiguration.Marker.class)
    public ExtPointBeanPostProcess extPointBeanPostProcess() {
        return new ExtPointBeanPostProcess();
    }

    /** 标记类, 无实质作用 */
    class Marker {
    }

}

四、总结

本文首先从中台概念讲起,以交易中台举例说明业务中台的现实意义。再进一步,讲述做业务中台百分百会遇到的平台通用逻辑和业务定制逻辑的矛盾冲突。顺着解决矛盾的几种思路,引入扩展点机制的讨论。最后,辅以源码来说明如何落地扩展点机制。

由于楼主的认识有限,有些点还未想透彻,比如: 1、扩展包做了修改后,如何做到不重启就能加载最新的扩展点。2、扩展包的可插拔管理和可视化;3、根据什么原则,判断是否需预留扩展点,扩展点的粒度最细到哪一层。期待大家发表看法,欢迎和楼主交流讨论。

PS: 本文的直接思路来源于《如何实现32.5万笔/秒的交易峰值?阿里交易系统TMF2.0技术揭秘》戳这里,源代码则借鉴了阿里的COLA框架戳这里,这两份参考资料也强烈建议读者查阅下。最后,献上楼主本篇中源码工程地址戳这里,建议工程Clone下来,对照单测Debug几次,5分钟就能上手!

你可能感兴趣的:(架构设计,业务中台,扩展点,交易中台)