给 COLA 做减法:应用架构中的“弯弯绕设计”

简介: COLA 的主要目的是为应用架构提供一套简单的可以复制、可以理解、可以落地、可以控制复杂性的”指导和约束"。在实践中作者发现 COLA 在简洁性上仍有不足,因此给 COLA 做了一次“升级”,在这次升级中,没有增加任何新的功能,而是尽量多删减了一些概念和功能,让 COLA 更简洁有效。

image.png

最近,同事告诉我,COLA 作为应用架构,已经被选入阿里云的 Java 应用初始化的应用架构选项之一。

image.png

This is really something,于是,在这个里程碑节点上,我开始回过头来,重新审视COLA 一路走来的得与失。

COLA 作为一种架构思想无疑是成功的。但是作为框架,个人感觉有点鸡肋之嫌。特别是在简洁性上做的不好,感觉做了不少画蛇添足的事情。

试想一下,有些功能我作为作者都很少去使用,我实在想不到,它为什么还有存在的理由。

基于上面的思考,我做了这一次 COLA 2.0 到 COLA 3.0 的升级。在本次升级中,我没有增加任何新的功能,而是尽量多删减了一些概念和功能。让 COLA 可以更加纯粹的 focus 在应用架构上,而不是框架支持和架构约束上。

支持我做这些决策的背后原因只有一个——奥卡姆剃刀原理。

奥卡姆剃刀原理

奥卡姆剃刀原理,是指如无必要,勿增实体(Entities should not be multiplied unnecessarily),即“简单有效原理”。正如奥卡姆在《箴言书注》2 卷 15 题说“切勿浪费较多东西去做,用较少的东西,同样可以做好的事情。”

在具体的应用过程中,我们可以遵循以下原则去做事情:

“如果同一个现象有 n 种理论,最简单的那个便是最正确的。能用 n 做好事情,那就不要有第 n+1 个动作。”

比如,《皇帝的新衣》的皇帝到底穿没穿衣服呢?如果你在现场,你很有可能就是大臣之一。

如果懂得了奥卡姆剃刀原理,可以用逻辑手段,判断谁是真理。

  • 第一种逻辑如下:假设皇帝是真的穿了衣服→假设愚蠢的人看不见→假设你就是愚蠢的人→所以你没看见皇帝穿衣服。
  • 第二种逻辑如下:假设皇帝没穿衣服→所以你没看见皇帝穿衣服。

同样看见光身子的皇帝,第二种解释简单明了。而第一种解释,可能正因为它是错误的,就需要更多假设来补救漏洞,就像说谎圆谎一样。

真相不需要伪装掩饰,简单明了。

再比如,地心说和日心说,托勒密的地心说模型是一个本轮均轮模型。人们可以按照这个模型,定量计算行星的运动,据此推测行星所在的位置。

到了中世纪后期随着观察仪器的不断改进,人们能够更加精确地测量出行星的位置和运动,观测到行星实际位置与这个模型的计算结果存在偏差,一开始还能勉强应付,后来小本轮增加到八十多个,却仍然不能精确地计算出行星的准确位置。

1543 年,波兰天文学家哥白尼在临终时发表了一部具有历史意义的著作——《天体运行论》。这个理论体系提出了一个明确的观点:太阳是宇宙的中心,一切行星都在围绕太阳旋转。该理论认为,地球也是行星之一,它一方面像陀螺一样自转,一方面又和其他行星一样围绕太阳转动。

image.png

哥白尼的计算不仅结构严谨,而且计算简单,与已经加到八十余个圈的地心说相比,哥白尼的计算与实际观测资料能更好地吻合。因此,地心说最终被日心说所取代。

设计中的弯弯绕

深入考察一下,我们系统中,类似于“地心说”这样的弯弯绕的设计,实在是不在少数。

从系统架构的角度看,有些弯弯绕是因为系统边界划分不合理,导致职责不清,依赖混乱。

从应用架构的角度看,有些弯弯绕是因为过度设计,为了追求所谓的灵活性和可扩展性,使用了不恰当的设计。导致本来可以直观呈现的代码逻辑,被各种包装,各种隐藏,各种转发.... 无形中极大的阻碍了代码的可读性和可理解性,增加了维护成本。

举个例子,我看过无数的业务系统,喜欢拿业务流程编排说事情。因此,在业务系统中,可以看到各种五花八门的“弯弯绕设计”。

比如,在一个业务系统中,我看到了如下的 pipeline 设计。这个设计的本质是说把一个复杂的业务操作进行结构化拆解为多个小的处理单元。

image.png

拆解是正确的,但是这种处理方式显然不够“奥卡姆”(关于更多结构化分解的内容,可以看我的另一篇文章:如何写复杂业务代码?)。作为维护人员,进入“入口函数”后,还要去查数据库,然后才能知道哪些组件被调用了,太绕了,不够直观,也不简洁。

同样的逻辑,按照下面的方式写不香吗?

public class CreateCSPUExecutor {
    @Resource
    private InitContextStep initContextStep;

    @Resource
    private CheckRequiredParamStep checkRequiredParamStep;

    @Resource
    private CheckUnitStep checkUnitStep;

    @Resource
    private CheckExpiringDateStep checkExpiringDateStep;

    @Resource
    private CheckBarCodeStep checkBarCodeStep;

    @Resource
    private CheckBarCodeImgStep checkBarCodeImgStep;

    @Resource
    private CheckBrandCategoryStep checkBrandCategoryStep;

    @Resource
    private CheckProductDetailStep checkProductDetailStep;

    @Resource
    private CheckSpecImgStep checkSpecImgStep;

    @Resource
    private CreateCSPUStep createCSPUStep;

    @Resource
    private CreateCSPULogStep createCSPULogStep;

    @Resource
    private SendCSPUCreatedEventStep sendCSPUCreatedEventStep;


    public Long create(MyCspuSaveParam myCspuSaveParam){
        SaveCSPUContext context = initContextStep.initContext(myCspuSaveParam);

        checkRequiredParamStep.check(context);

        checkUnitStep.check(context);

        checkExpiringDateStep.check(context);

        checkBarCodeStep.check(context);

        checkBarCodeImgStep.check(context);

        checkBrandCategoryStep.check(context);

        checkProductDetailStep.check(context);

        checkSpecImgStep.check(context);

        createCSPUStep.create(context);

        createCSPULogStep.log(context);

        sendCSPUCreatedEventStep.sendEvent(context);

        return context.getCspu().getId();
    }
}

这种写法简单直观,易维护,与前一种方式相比,具有同样的组件复用性。符合奥卡姆剃刀的精神,相比较而言,前面那种弯弯绕设计,虽然看起来有点设计感,带来了一点点 OCP 的好处。但是无端增加了理解和认知成本,孰优孰劣,不难分辨。

COLA 3.0 升级

做了这么长的铺垫,终于到了批斗 COLA 中“弯弯绕设计”的时候了。

去掉 Command

在 COLA 的初始阶段,因为受到 CQRS 的影响,于是想到了使用命令模式来处理用户请求。设计的初衷是想通过框架,一方面强制约束 Command 和 Query 的处理方式,另一方面把 Service 里面的逻辑,强制拆分到 CommandExecutor 中去,防止 Service 膨胀过快。

和上面介绍过的 pipeline 设计类似,这种设计有点绕,不够直观,如下所示:

public class MetricsServiceImpl implements MetricsServiceI{

    @Autowired
    private CommandBusI commandBus;

    @Override
    public Response addATAMetric(ATAMetricAddCmd cmd) {
        return commandBus.send(cmd);
    }

    @Override
    public Response addSharingMetric(SharingMetricAddCmd cmd) {
        return commandBus.send(cmd);
    }

    @Override
    public Response addPatentMetric(PatentMetricAddCmd cmd) {
        return  commandBus.send(cmd);
    }

    @Override
    public Response addPaperMetric(PaperMetricAddCmd cmd) {
        return  commandBus.send(cmd);
    }
}

看起来还挺干净的,可是 ATAMetricAddCmd 到底是被哪个 Executor 处理的呢,不直观。我还要去理解 CommandBus,以及 CommandBus 是如何注册 Executor 的。无形中增加了认知成本,不好。

既然这样,为何不用奥卡姆剃刀把这个 CommandBus 剔除呢。如下所示,去除 CommandBus 之后,代码是不是直观了很多,唯一的损失是我们会失去框架层面提供的 Interceptor 功能,然而,Interceptor 正是我下一个要动刀的地方。

public class MetricsServiceImpl implements MetricsServiceI{

    @Resource
    private ATAMetricAddCmdExe ataMetricAddCmdExe;
    @Resource
    private SharingMetricAddCmdExe sharingMetricAddCmdExe;
    @Resource
    private PatentMetricAddCmdExe patentMetricAddCmdExe;
    @Resource
    private PaperMetricAddCmdExe paperMetricAddCmdExe;

    @Override
    public Response addATAMetric(ATAMetricAddCmd cmd) {
        return ataMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addSharingMetric(SharingMetricAddCmd cmd) {
        return sharingMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addPatentMetric(PatentMetricAddCmd cmd) {
        return  patentMetricAddCmdExe.execute(cmd);
    }

    @Override
    public Response addPaperMetric(PaperMetricAddCmd cmd) {
        return  paperMetricAddCmdExe.execute(cmd);
    }
}

去掉 Interceptor

当时设计 Interceptor,是因为有 CommandBus 作为基础,为了更好的利用命令模式带来的好处,便添加了 Interceptor 功能。其本质是一个 AOP 处理。

鉴于 Spring 的 AOP 功能已经很完善了,这个设计也是有点鸡肋。事实证明,大家在使用 COLA 框架的时候,很少会使用 Interceptor,包括我自己也是一样。既然如此,剔除也罢。

去掉 Convertor、Validator、Assembler

关于命名的重要性,这里就不赘述了。当时想着是否能从框架层面,规范一下一些常用功能的命名。但是在实际使用中,发现这个想法也是有些过于理想化了。

我记得,在团队实践 COLA 的初期,还经常为什么是 Convertor(转换器),什么是 Assembler(组装器)的事情,争论不休。

后面我仔细想了想,命名虽然很重要,但其作用域最多也就是一个团队规范,你校验器是叫 Validator 还是 Checker 并没有什么本质区别,团队自己定义就好了。尝试从框架层面去解决团队约定问题,其效果不会太好,因此也果断挥刀剔除。

类扫描优化

业务身份和扩展点的思想,是 TMF 的核心理念,也是阿里业务中台的进行多业务支持的核心方法论。

COLA 致力于提供一种轻量级的扩展实现方式,因此该功能在奥卡姆的屠刀下得以保存。因为 COLA 的扩展点设计是借鉴了中台的 TMF,因此在前面的设计中,其类扫描方案是直接照搬 TMF 的做法。

实际上,TMF 的类扫描方案对 COLA 来说有点多余。因为 COLA 本身就是架设在 Spring 的基础之上,而 Spring 又是建立在类扫描的基础之上。因此,我们完全可以复用 Spring 的类扫描,没必要自己写一套。

在原生的 Spring 中,至少有 3 种方式可以获取到用户自定义 Annotation 的 Bean,最简洁的是通过 ListableBeanFactory.getBeansWithAnnotation 方法,或者使用 ClassPathScanningCandidateComponentProvider 进行扫包。

在这次改版中,我选用的是 getBeansWithAnnotation 方法,主要是为了获取 @Extension 的 Bean,用来实现扩展点功能,废弃了原来的 TMF 类扫描实现。

总结

触发这次升级的动机,主要是因为,自己在实践 COLA 的过程中,的确发现有些华而不实的功能。在 COLA 作为阿里云的基础应用架构,其影响力越来越大的时候,我有责任给到大家一个正确的引导——去伪存真,简洁有效,而不是引入更多的复杂度。

实际上,COLA 是由两部分组成的:

一方面 COLA 是一种架构思想,是整合了洋葱圈架构、适配器架构、DDD、整洁架构、TMF 等架构思想的一种应用架构。

image.png

在这次升级中,架构思想部分基本没有变化,唯一一点是因为去除了 Command 概念,因此 CQRS 也成了可选项,而不再是一种强要求。

另一方面 COLA 也是框架组件,通过这次升级,我使用奥卡姆剃刀砍掉了绝大部分的组件能力,仅仅保留了扩展点功能。其用意是不希望 COLA 作为框架给到应用开发者太多的约束,这不符合简单有效的风格。

所以,总结下来,与其说这是一次升级,不如说它是功能“降级”,是在做减法。

但我相信,减法可以让 COLA 更加符合奥卡姆精神,帮助 COLA 轻装上阵,走的更远。

COLA 开源地址:

https://github.com/alibaba/COLA

目录
相关文章
|
1天前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
3天前
|
存储 监控 前端开发
掌握微前端架构:构建未来前端应用的基石
【10月更文挑战第12天】随着前端技术的发展,传统的单体应用架构已无法满足现代应用的需求。微前端架构通过将大型应用拆分为独立的小模块,提供了更高的灵活性、可维护性和快速迭代能力。本文介绍了微前端架构的概念、核心优势及实施步骤,并探讨了其在复杂应用中的应用及实战技巧。
|
2天前
|
运维 Go 开发者
Go语言在微服务架构中的应用与优势
本文深入探讨了Go语言在构建微服务架构中的独特优势和实际应用。通过分析Go语言的核心特性,如简洁的语法、高效的并发处理能力以及强大的标准库支持,我们揭示了为何Go成为开发高性能微服务的首选语言。文章还详细介绍了Go语言在微服务架构中的几个关键应用场景,包括服务间通信、容器化部署和自动化运维等,旨在为读者提供实用的技术指导和启发。
|
4天前
|
设计模式 测试技术 持续交付
架构视角下的NHibernate:设计模式与企业级应用考量
【10月更文挑战第13天】随着软件开发向更复杂、更大规模的应用转变,数据访问层的设计变得尤为重要。NHibernate作为一个成熟的对象关系映射(ORM)框架,为企业级.NET应用程序提供了强大的支持。本文旨在为有一定经验的开发者提供一个全面的指南,介绍如何在架构层面有效地使用NHibernate,并结合领域驱动设计(DDD)原则来构建既强大又易于维护的数据层。
17 2
|
5天前
|
存储 消息中间件 人工智能
ApsaraMQ Serverless 能力再升级,事件驱动架构赋能 AI 应用
本文整理自2024年云栖大会阿里云智能集团高级技术专家金吉祥的演讲《ApsaraMQ Serverless 能力再升级,事件驱动架构赋能 AI 应用》。
|
2天前
|
Kubernetes 监控 持续交付
深入理解微服务架构及其在现代应用开发中的应用
【10月更文挑战第15天】深入理解微服务架构及其在现代应用开发中的应用
9 0
|
4天前
|
存储 前端开发 数据库
一文搞懂SaaS应用架构:应用服务、应用结构、应用交互设计
【10月更文挑战第21天】本文介绍了 SaaS 应用服务的多租户服务、安全服务和更新与维护服务,以及 SaaS 应用的前后端结构和交互设计。多租户服务涉及数据隔离和资源分配;安全服务包括身份认证与授权及数据安全;更新与维护服务涵盖版本管理和技术支持。前端结构关注用户界面设计和前端技术选型;后端结构则涉及微服务架构和数据库管理。交互设计强调租户与应用的交互和应用内部模块间的交互。
|
12天前
|
缓存 监控 API
探索微服务架构中的API网关模式
【10月更文挑战第5天】随着微服务架构的兴起,企业纷纷采用这一模式构建复杂应用。在这种架构下,应用被拆分成若干小型、独立的服务,每个服务围绕特定业务功能构建并通过HTTP协议协作。随着服务数量增加,统一管理这些服务间的交互变得至关重要。API网关作为微服务架构的关键组件,承担起路由请求、聚合数据、处理认证与授权等功能。本文通过一个在线零售平台的具体案例,探讨API网关的优势及其实现细节,展示其在简化客户端集成、提升安全性和性能方面的关键作用。
45 2
|
16天前
|
存储 缓存 监控
探索微服务架构中的API网关模式
【10月更文挑战第1天】探索微服务架构中的API网关模式
48 2
|
1月前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2