本文主要是介绍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
...
充血模型
充血模型的核心是文件级别的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 logicreturn {...self,//mutate some thing}
}
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'}
}
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 logicconst c = someThingToAdapt(a)
// more common logic
}export const commonLogic = (a: Entity1, b: Entity2):Entity3 => {}
Aggregate 同理,只是先聚合了一些实体再提供服务。
前后端同构
nextjs这种框架非常好的提供了前后端同构的机会,特别是再利用tRPC抹平网络请求的话,同构会非常舒服。而且这种同构天然符合DDD的领域和限界上下文的理念,无成本的让相同的命名、行为在不同的端上复用。
以entity举例来说,一个entity一定会有属性和方法。这两部分都会有同构(业务的核心复杂度)和异构(前后端各自的偶然复杂度)的地方。那么文件结构和代码应该如下组织:
some-entity/entity.ts
export interface SomeEntity {coreProp: string;coreFunc1(self: SomeEntity);
}export const coreFunc2 = (self: SomeEntity) => {}
some-entity/fe/entity.ts
export type SomeEntityFe = SomeEntity & {feProp: number;feFunc1(self: SomeEntityFe);
}export const feFunc2 = (self: SomeEntityFe) => {}
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;
tsx同理。
总结
nextjs的同仓开发能带来非常好的领域/限界上下文代码共享能力。再利用好factory和typedef,可以以领域为维度组织起一整套不论是DDD还是ts视角都很合理的架构。
这篇关于nextjs上的DDD架构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!