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 logic
return {
...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 logic
const 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视角都很公道的架构。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]