• nextjs上的DDD架构


    背景

    新入职公司,需要快速把之前杂乱无章的首页(有复杂业务,nextjs)搭一个靠谱的架构,否则基本没办法把事情继续推进了(核心流程需要持续大量适配到不同的后端实现上)。
    个人客户端出身,之前落地DDD都是在正经强类型、静态类型语言上,而nextjs(ts)上语言习俗与DDD模式格格不入,遂制定了一些ts友好的规则来落地DDD。

    DDD与ts

    DDD的核心组成是充血对象(entity)+immutable 对象(vo)+整合服务(domain service/aggregate)。当然还有领域、限界上下文这种更抽象的原则。
    非常非常OOP的理念,希望利用类结构建模真实世界。而且经常会利用多态来做类型行为变化,进而很容易做适配。
    BUT,ts世界里,经过this的暴虐和hooks的大行其道,类、多态、实现接口都是异类。

    落地

    核心目的是让nextjs里能用原汁原味的ts写出来原教旨的DDD。同时,达成高效适配不同后端和高效的前后端统一话术。

    文件夹结构

    nextjs的一级目录默认是按技术分类的,这个是为了框架的实现成本。但是,既然引入DDD,那么除了方便框架的api和pages,其他目录一定是按业务的架构来组织。绝对不能按技术持续划分下去。

    |-api
    |-pages
    |-domain0
    	|- context0
    		|- entity0.ts
    		|- entity1
    			|- entity-p0.ts
    			|- entity-p1.ts
    		|- service0.ts
    |-domain1
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    充血模型

    充血模型的核心是文件级别的solid,数据与行为在同一个文件中。具体业务的迭代会只发生在这个文件中。换句话说,与这个模型相关的知识仅存在于这个文件中。当然,为了保持文件长度可控,这个“文件”也可能是一个文件夹+一个index文件。

    几个细节要注意:

    • 多态是靠factory产生不同对象来实现的
    • 而这些伪多态方法的第一个入参必须是self
    • 虽然entity是mutable的,但是entity对象仍然保持immutable,所有变化都返回一个新对象

    entity.ts

    export interface SomeEntity {
    	someProp: string;
    	yaProp: number;
    
    	somePloyFunc(self: SomeEntity);
    }
    
    export const someFunc = (self: SomeEntity): SomeEntity => {
    	// some logic
    	return {
    		...self,
    		//mutate some thing
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    factory.ts

    import {implType1} from 'adapter1'
    import {implType2} from 'adapter2'
    
    export const someEntityFactory = (input: any): SomeEntity => {
    	const { type } = input;
    	if (type === type1) {
    		return {
    			...input,
    			somePloyFunc: implType1
    		}
    	} else if (type === type2) {
    		return {
    			...input,
    			somePloyFunc: implType2
    		}
    	} else {
    		throw 'unknown type'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    VO也可以用差不多的逻辑,但是由于其immutable和用后即抛的特质,interface应该就足够了。

    领域服务

    普通领域服务其实就是取好名字,export一个function就好了,入参就是entity和VO。
    但是,涉及到多实现适配就很难用interface+impl的方式实现了。这里要仿照react的useProp来处理。这种逻辑一定是有三部分组成的:标准的整体流程调度,不同的具体实现细节和实现细节间共享的逻辑。

    export const simpleService = (a: Entity1, b: Entity2):Entity3 => {
    ...
    }
    
    export const useAdaptedService = (a: Entity1, b: Entity2, someThingToAdapt:((a: Entity1)=>Entity3))=> {
    // common logic
    	const c = someThingToAdapt(a)
    // more common logic
    }
    
    export const commonLogic = (a: Entity1, b: Entity2):Entity3 => {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Aggregate 同理,只是先聚合了一些实体再提供服务。

    前后端同构

    nextjs这种框架非常好的提供了前后端同构的机会,特别是再利用tRPC抹平网络请求的话,同构会非常舒服。而且这种同构天然符合DDD的领域和限界上下文的理念,无成本的让相同的命名、行为在不同的端上复用。
    以entity举例来说,一个entity一定会有属性和方法。这两部分都会有同构(业务的核心复杂度)和异构(前后端各自的偶然复杂度)的地方。那么文件结构和代码应该如下组织:
    some-entity/entity.ts

    export interface SomeEntity {
    	coreProp: string;
    
    	coreFunc1(self: SomeEntity);
    }
    
    export const coreFunc2 = (self: SomeEntity) => {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    some-entity/fe/entity.ts

    export type SomeEntityFe = SomeEntity & {
    	feProp: number;
    	
    	feFunc1(self: SomeEntityFe);
    }
    
    export const feFunc2 = (self: SomeEntityFe) => {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    some-entity/bff/entity.ts 与fe类似。
    其中的coreFunc1的前端实现是请求后端,后端的实现是真正的业务逻辑,靠tRPC桥接。

    前后端异构

    还有一波前后端异构的部分是api和react component的实现。这些只有一个要求:除了最外层的整合,都放到domain下。这样,domain可以认为是完美闭包的,复用和导出的成本为零,迭代时做权限管理也只需要关注domain下的路径:前端负责domain//fe/的代码,bff负责domain//bff/,业务架构师负责domain目录下其他部分。
    api/domain/entity/some-action.ts

    export const handle = (req) => {
    	const a: Entity1Bff = entity1Factory(req);
    	const b: Entity2Bff = entity2Factory(req);
    	const imp = req.xxx?imp1:imp2;
    	const result = useAdaptedService(a, b, imp);
    	return Response();
    }
    
    export const POST = handle;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    tsx同理。

    总结

    nextjs的同仓开发能带来非常好的领域/限界上下文代码共享能力。再利用好factory和typedef,可以以领域为维度组织起一整套不论是DDD还是ts视角都很合理的架构。

  • 相关阅读:
    IDEA显示val,var的推断类型的设置
    【附源码】Python计算机毕业设计商场VIP管理系统
    web3 入门记录
    【Vue十二】--- 指令
    Java微服务+分布式+全栈项目(一)---->项目介绍+MyBatis-Plus入门
    iOS小技能:Xcode14新特性(适配)
    通过哈密顿蒙特卡罗(HMC)拟合深度高斯过程,量化信号中的不确定性
    JavaWeb-jdbc的mysql驱动问题
    uniapp手机一键登录,微信授权登陆
    使用ChatGLMTokenizer处理json格式数据
  • 原文地址:https://blog.csdn.net/pouloghost/article/details/136590684