写代码原来如此简单:两种常用代码范式

简介: 一次项目包含非常多的流程,有需求拆解,业务建模,项目管理,风险识别,代码模块设计等等,如果我们在每次项目中,都将精力大量放在这些过程的思考上面,那我们剩余的,放在业务上思考的精力和时间就会大大减少;这也是为什么我们要 总结经验/方法论/范式 的原因;这篇文章旨在建立代码模块设计上的思路,给出了两种非常常用的设计范式,减少未来在这一块的精力开销。

1.领域模型驱动的代码范式

领域模型驱动的代码范式,是围绕着领域知识设计的,需要先理解业务模型,再将业务模型映射到软件的对象模型中来;本章节重点在我们有了业务模型之后的代码模式,具体业务模型如何构建在《架构之道:人人都是架构师》中有详细讨论;


image.png


上图中间就是该模式最重要的领域,领域层代码作为系统的最核心资产模块,可以被打包迁移到任何应用上,而不关心具体的三方服务提供方和具体的持久化方案,即外部服务的变化对领域层代码是没有任何侵入的;


从上图来看,领域对象(ENTITY、AGGREGATION、VALUEOBJECT、MODEL)用于描述业务模型,是业务关系最重要的体现;为了屏蔽持久化方案的细节,我们使用仓库(REPOSITORY)来查询和持久化领域对象;有的时候我们期望直接获取到一个非空的领域对象,而不关心这个对象从哪里来,如何构造,那我们就需要工厂(FACTORY)来帮我们生产这个对象;当领域内依赖外部服务能力时,需要门面(FACADE)帮助我们屏蔽具体的服务提供方;


有了以上这些模型对象和基础能力模块,我们需要领域服务(DomainServie)层作为“上帝之手”帮我们编排具体的业务逻辑;本章对领域服务有更细的三层划分,第一层是实体操作服务(BaseUpdateDomainService),用于收敛操作实体/聚合的真实变更行为;第二层是业务流程服务(BaseBizDomainService),用于收敛基础的有业务语义的行为;第三层是用例领域服务(UserCaseDomainService),用于映射具体的业务需求用例场景。


1.1 领域模型对象

image.png 


  • 实体

由标识定义,而不依赖它的所有属性和职责,并在整个生命周期中有连续性。这句话在初看的时候非常晦涩,简单来说,就是一个标识没变的对象,在其他自身属性发生变化后,它依然是它,那么它就是实体;以上图为例,一个商品的商品id没变,即使它的标题改了,图片改了,优惠信息变了,它发生了翻天覆地的变化,但它依然是它,它有唯一的标识来表明它还是它,只是一些属性发生了变化;通过这种方式来识别实体的目的,是因为领域中的关键对象,通常并不由它们的属性定义,而是由可见的/不可见的标识来定义,且有完整的生命周期,在这个周期内它如何变化,它都依然是它;通过这种方式识别出实体这种领域关键对象,也是领域驱动设计和数据驱动设计最大的差别,数据驱动设计是先识别出我们需要哪些数据表,然后将这些数据表映射为对象模型;而领域驱动设计是先通过业务模型识别出实体,再将实体映射为所需要的数据表。


  • 值对象

用于描述领域的某个方面而本身没有唯一标识的对象。被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。如上图举个例子,一个商品实体的发货地址Address对象有区域信息、门牌信息、时区信息这几个属性,其中的门牌号从111修改成222后,它就已经不再是修改前的那个它了,因为门牌号222并不等于门牌号111的地址。即它是没有生命周期的,它的equals方法由它的属性值决定(实体的equals方法由唯一标识决定);


  • 聚合

聚合是一组实体和值对象的组合;内部包含一个聚合根,和由聚合根关联起来的实体和值对象;以上图为例,有商品、优惠、库存这三个实体和地址这一个值对象,对于一个商品而言,完整的商品信息需要包含优惠、库存、地址这些信息,那么在商品模型中,商品就是聚合根,其内部通过优惠id关联它的优惠信息,通过库存id关联商品的库存信息;聚合将这组关联关系建立,对外提供统一的操作,比如需要删除某个商品,那么这个聚合的内部可以在一个事务(或分布式事务)中,对库存进行清空,对优惠进行清理,最终对商品进行删除。


1.2 查询/构造能力


image.png


  • 仓库

仓库是可持久化的领域对象和真实物理存储操作之间的媒介,随意的数据库查询会破坏领域对象的封装,所以需要抽象出仓库这种类型,它定义领域对象的获取和持久化方法,具体实现不由领域层感知;至于具体用了什么存储,如何写入和查询,是否使用缓存,这些逻辑统一封装在仓库的实现层,对于后续迁移存储、增删缓存,都可以做到不侵蚀业务领域。


比如整个领域模块需要打包给海外业务使用,在海外我们需要用当地的存储,那么这个迁移对于领域层是没有侵入的,只需要在基础设施层修改仓库的实现即可;


再比如我们的数据库存在性能瓶颈,需要在数据库上增加一层缓存,这个操作对领域也是没有侵入的,只需要在仓库的实现处,增加缓存的读写即可,对业务逻辑无感。


  • 三方能力门面

门面用于封装第三方的能力,设计初衷本质和仓库是一样的,目的都是屏蔽具体的三方能力实现,让稳定的领域层不去依赖无法把控变化方向的第三方;上图中的三个case是比较经典的三个例子:


  • 我们的模型中依赖外部查询获取的商品模型,这个模型中有商品标题、商品图片、店铺名称这几个信息,那么我们需要在Domain层定义一个商品类ItemInfo,包含这几个属性,然后在Domain层定义一个获取ItemInfo对象的服务接口,比如叫ItemFacade,方法是getItemInfo;接下来我们需要在Infrastructure层实现Domain层定义的这个接口,比如具体的实现是依赖Ic的接口,将ItemDO转换为Domain层的ItemInfo;可以发现,这样的设计让Domain对商品信息的获取源是无感的,当我们的能力需要部署到海外,或者IC某一天进行了重大改革,需要对模型进行大改,那么我们只需要重新实现Infrastructrue中ItemFacadeImpl即可。这个思想其实就是依赖倒置的思想,稳定不应该依赖变化,变化应该依赖稳定;因为第三方的变化方向是无法把控的,它的变化不应该侵入到我们的领域知识内部。


  • 我们的领域还依赖一些消息发送、限流等基础功能,也需要在Domain层定义相应能力的Facade,在Infrastructure层实现,目的同上,将metaq替换成notify/swift时,领域层是无感的。


  • 工厂


当创建一个实体对象或聚合的操作很复杂,甚至有很多领域内部的结构需要暴露的时候,就可以用工厂进行封装。一种相对简单粗暴的判断方法是看这个类的构造方法实现是否复杂,并且看着这些逻辑不应该由这个类实现,那么不妨用工厂来构造这个对象吧!


1.3 领域服务


image.png

有一些对实体/聚合/值对象进行编排操作的概念并不适合被建模为对象,那么它应该被抽象为领域服务,化作一只上帝之手,做领域对象间流程操作的编排。服务很重要的特征,它的操作应该是无状态的。本文基于开发实践,对领域服务做了三层更细节的划分:


  • 实体操作服务


即图中的BaseUpdateDomainService,是最基础的一类领域服务,用于收口某个实体的真实物理操作,它的流程中一般包含一个核心的update/insert操作,作用在写数据库上,依情况可以增加:


BeforeUpdateHandlers和AfterUpdateHandlers,用于更新前后的一些额外业务操作;


如1.1中的图,我们在内存中操作完某个库存实体对象,需要更新db的时候,可以调用它对应的服务,在这个服务中,我们除了将变更的值更新db,还需要对外发送消息,更新前也需要执行一些校验的回调,那么校验回调可以放在BeforeUpdateHandler中,对外的消息可以放在AfterUpdateHandler中;抽象出这样的一个服务,好处是可以收敛最基础的变更操作,不至于不同的入口对某个对象的更新,还会出现不一样的操作(比如需要发送更新消息,不同的入口操作更新,有的发送消息,有的不发送)。


一般每个实体都需要有一个对应的操作服务(或者模型极其简单可以省略这一层),操作服务可以依赖其他的操作服务,比如1.1中商品模型的更新,是需要依赖库存更新服务和优惠更新服务的。


  • 业务流程服务

这一层的领域服务对应图中的BaseBizDomainService,用于收敛一些通用的业务流程,可以直接对接业务接口或者用于上层的用例编排;比如我们在商家请求、接到第三方消息、具体的某几个用例中,需要先查询ItemFacade,然后进行一些业务逻辑判断,然后根据情况对1.1商品模型中的优惠进行清理,那么就可以将这段逻辑收敛到“XX优惠清理领域服务”中,在多个上层场景需要进行该操作时,直接调用这个领域服务即可。


  • 用例领域服务

这一层对应图中的UserCaseDomainService作为领域服务的最上层,也是最具体的一层,用于实现 定制的/不可复用的 用例场景业务逻辑,只直接对接对外的api。


1.4 包结构实践


image.png


该包结构即描述了上述的所有模块,其中infrastructure模块依赖domain模块;domain模块的pom文件理论上不应该有任何三方依赖(除了一些工具类)。



2.过程驱动的代码范式

领域驱动的代码,重点是抽象领域模型,沉淀领域对象实体,它用模型间的关系以及模型直接的操作来沉淀知识;过程驱动的代码,重点是抽象能力,沉淀函数,并用编排引擎串联执行过程,实现对知识的描述。


在面向对象大张旗鼓的今天,大多数人对面向过程编程嗤之以鼻,但有些场景使用过程驱动的编程思路,反而能更好地描述业务规则以及业务流程,比如前台表达的渲染链路,或是章节一中 比较重的领域服务,使用过程驱动能更好地描述数据处理的过程以及产品用例流程。


image.png

上图能力库中的能力点是我们过程驱动最核心的部分,是我们对能力的抽象,一般一个能力只沉淀一个具体的原子方法,并决策流程是否能执行下去;往上是阶段划分,不同的能力在具体的业务流程中,是处于不同阶段的,这里的阶段划分是指从流程阶段的维度对能力点进行分类并放在不同的包中,让我们的流程更加清晰;再往上是不同场景下我们对能力点的执行链路编排,并对外做统一输出。


2.1 能力点

能力点通常由一个接口定义,入参是执行上下文,出参是流程是否需要继续。


public interface AbilityNode<C extends Context> {

    /**
     * 执行一个渲染节点
     * @param context 执行上下文
     * @return 是否还需要继续往下执行
     */
    boolean execute(C context);


基于这个接口,我们可以实现非常多原子能力节点。java中,这些能力节点作为bean由spring容器统一管理,运行时取出即用。


在一个业务场景下,我们的能力点往往会非常多,那么我们就需要对他们进行基于业务场景的阶段划分,并分门管理;比如我们在前台投放场的实践中,按照召回、补全、过滤、排序、渲染,划分了五个阶段,每一个能力点被归类到其中一个阶段中进行管理。


2.2 能力编排

有了能力点,我们需要基于编排引擎将这些能力串联起来,用以描述业务规则或是业务流程;这些能力执行的过程有些仅仅是可串行执行的,有些是也可以并行执行的,下面给出一套通用的流程协议以及实现过程:


public abstract class AbilityChain<C extends Context<?>> {

    @Resource
    private ThreadPool threadPool;

    public abstract C initContext(Request request);

    public Response execute(Request request) {
        //获取渲染上下文
        C ctx;
        try {
            ctx = this.initContext(request);
            if (ctx == null || ctx.getOrder() == null || CollectionUtils.isEmpty(ctx.getOrder().getNodeNames())) {
                return null;
            }
        } catch (Throwable t) {
            log.error("{} catch an exception when getRenderContext, request={}, e="
                    , this.getClass().getName(), JSON.toJSONString(requestItem), t);
            return null;
        }
        try {
            //执行所有节点
            for (List<String> nodes : ctx.getOrder().getNodeNames()) {
                List<AbilityNode<C>> renderNodes = renderNodeContainer.getAbilityNodes(nodes);
                boolean isContinue = true;
                if (renderNodes.size() > 1) {
                    //并发执行多个节点
                    isContinue = this.concurrentExecute(renderNodes, ctx);
                } else if (renderNodes.size() == 1){
                    isContinue = this.serialExecute(renderNodes, ctx);
                }
                if (!isContinue) {
                    break;
                }
            }
            
            return ctx.getResponse();
        } catch (Throwable t) {
            log.error("RenderChain.execute catch an exception, e=", t);
            return null;
        }
    }

    /**
     * 并发执行多个node,如果不需要继续进行了则返回false,否则返回true
     */
    private boolean concurrentExecute(List<AbilityNode<C>> nodes, C ctx) {
        if (CollectionUtils.isEmpty(nodes)) {
            return true;
        }
        long start = System.currentTimeMillis();
        Set<Boolean> isContinue = Sets.newConcurrentHashSet();
        List<Runnable> nodeFuncList = nodes.stream()
            .filter(Objects::nonNull)
            .map(node -> (Runnable)() -> isContinue.add(this.executePerNode(node, ctx)))
            .collect(Collectors.toList());
        linkThreadPool.concurrentExecuteWaitFinish(nodeFuncList);
        //没有node认为不继续,就是要继续
        return !isContinue.contains(false);
    }
    
    /**
     * 串行执行多个node,如果不需要继续进行了则返回false,否则返回true
     */
    private boolean serialExecute(List<AbilityNode<C>> nodes, C ctx) {
        if (CollectionUtils.isEmpty(nodes)) {
            return true;
        }
        for (AbilityNode<C> node : nodes) {
            if (node == null) {
                continue;
            }
            boolean isContinue = this.executePerNode(node, ctx);
            if (!isContinue) {
                //不再继续执行了
                return false;
            }
        }
        return true;
    }
    
    /**
     * 执行单个渲染节点
     */
    private boolean executePerNode(AbilityNode<C> node, C context) {
        if (node == null || context == null) {
            return false;
        }
        try {
            boolean isContinue = node.execute(context);
            if (!isContinue) {
                context.track("return false, stop render!");
            }
            return isContinue;
        } catch (Throwable t) {
            log.error("{} catch an exception, e=", nodeClazz, t);
            throw t;
        } 
    }

}


其中流程协议为:


[
    [
        "Ability1"
    ],
    [
        "Ability3",
        "Ability4",
        "Ability6"
    ],
    [
        "Ability5"
    ]
]


其中Ablitiy3 4 6表示需要并发执行,Ability1、3/4/6、5表示需要串行执行。


2.3 切面

有了能力点和流程编排引擎,基于过程编码的代码骨架就已经有了;但是往往我们还需要一些无法沉淀为能力的逻辑,需要在每个节点(或指定节点)执行前/后进行,这就需要切面的能力;


image.png

如图,比如流程埋点、预校验就非常适合放到切面这一层实现。下图是我们在实践中,基于切面实现的能力点追踪,可以清晰地看到每个能力点执行的过程以及数据变更情况。



image.png


2.4 包结构实践

image.png

该包结构即描述了上述的所有模块,chain目录下为多个业务场景流程,围绕着外层的AbilityNode和AbilityChain进行实现,编排出符合业务场景逻辑的流程。



3.写在后面

文章中的代码范式可以应用在大部分场景,在项目初起的时候直接套用,可以省下大部分关于包模块划分的思考精力,并且在后续迭代中,团队统一规范,持续按照这个框架演进,可以让代码更加井井有条,减少一些诡异的类职责划分问题。




来源  |  阿里云开发者公众号

作者  |  谨寻



相关文章
|
2天前
|
调度 云计算 芯片
云超算技术跃进,阿里云牵头制定我国首个云超算国家标准
近日,由阿里云联合中国电子技术标准化研究院主导制定的首个云超算国家标准已完成报批,不久后将正式批准发布。标准规定了云超算服务涉及的云计算基础资源、资源管理、运行和调度等方面的技术要求,为云超算服务产品的设计、实现、应用和选型提供指导,为云超算在HPC应用和用户的大范围采用奠定了基础。
|
9天前
|
存储 运维 安全
云上金融量化策略回测方案与最佳实践
2024年11月29日,阿里云在上海举办金融量化策略回测Workshop,汇聚多位行业专家,围绕量化投资的最佳实践、数据隐私安全、量化策略回测方案等议题进行深入探讨。活动特别设计了动手实践环节,帮助参会者亲身体验阿里云产品功能,涵盖EHPC量化回测和Argo Workflows量化回测两大主题,旨在提升量化投研效率与安全性。
云上金融量化策略回测方案与最佳实践
|
11天前
|
人工智能 自然语言处理 前端开发
从0开始打造一款APP:前端+搭建本机服务,定制暖冬卫衣先到先得
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。
8830 20
|
15天前
|
Cloud Native Apache 流计算
资料合集|Flink Forward Asia 2024 上海站
Apache Flink 年度技术盛会聚焦“回顾过去,展望未来”,涵盖流式湖仓、流批一体、Data+AI 等八大核心议题,近百家厂商参与,深入探讨前沿技术发展。小松鼠为大家整理了 FFA 2024 演讲 PPT ,可在线阅读和下载。
4757 12
资料合集|Flink Forward Asia 2024 上海站
|
15天前
|
自然语言处理 数据可视化 API
Qwen系列模型+GraphRAG/LightRAG/Kotaemon从0开始构建中医方剂大模型知识图谱问答
本文详细记录了作者在短时间内尝试构建中医药知识图谱的过程,涵盖了GraphRAG、LightRAG和Kotaemon三种图RAG架构的对比与应用。通过实际操作,作者不仅展示了如何利用这些工具构建知识图谱,还指出了每种工具的优势和局限性。尽管初步构建的知识图谱在数据处理、实体识别和关系抽取等方面存在不足,但为后续的优化和改进提供了宝贵的经验和方向。此外,文章强调了知识图谱构建不仅仅是技术问题,还需要深入整合领域知识和满足用户需求,体现了跨学科合作的重要性。
|
23天前
|
人工智能 自动驾驶 大数据
预告 | 阿里云邀您参加2024中国生成式AI大会上海站,马上报名
大会以“智能跃进 创造无限”为主题,设置主会场峰会、分会场研讨会及展览区,聚焦大模型、AI Infra等热点议题。阿里云智算集群产品解决方案负责人丛培岩将出席并发表《高性能智算集群设计思考与实践》主题演讲。观众报名现已开放。
|
11天前
|
人工智能 容器
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
本文介绍了如何利用千问开发一款情侣刮刮乐小游戏,通过三步简单指令实现从单个功能到整体框架,再到多端优化的过程,旨在为生活增添乐趣,促进情感交流。在线体验地址已提供,鼓励读者动手尝试,探索编程与AI结合的无限可能。
三句话开发一个刮刮乐小游戏!暖ta一整个冬天!
|
10天前
|
消息中间件 人工智能 运维
12月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
871 57

热门文章

最新文章