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视角都很合理的架构。

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值