哈Ha!最近,我一直非常关注建筑,并决定与社区分享由AndréBazaglia撰写的文章带有Typescript的清洁建筑:DDD,洋葱的翻译。介绍
在我超过6年的专业经验中,我有机会在很酷的技术公司工作,这些公司非常关注高可用性和代码质量。当错误或什至第二次系统停机无法接受时,我不得不处理紧急情况。本文的目的不是详细介绍DDD和分层体系结构上的复杂主题,而是在Typescript中实现这两种方法的示例。所使用的项目是基础项目,可以使用CQRS方法进行完善和扩展。为什么要DDD?
创建的软件产品必须实现设定的业务要求。这种方法简化了域层的测试,使您可以确保考虑到所有业务需求并编写长期有效的防错代码。当在一个位置更改代码会导致在不同位置出现无数错误时,将DDD与分层体系结构结合使用可避免多米诺骨牌效应。为什么要分层架构?
这种方法与DDD配合得很好,因为整个系统都是围绕领域模型构建的,领域模型是下图中的中心圆。内层不直接依赖于外层,而是通过注入使用它们。另外,我们获得了出色的灵活性,因此每一层都有自己的责任范围,传入数据由每一层根据其需求进行验证。因此,内层始终从外层接收有效数据。更不用说测试:通过使用基于接口的依赖项模拟,单元测试变得更加容易,这使您可以从系统的外部(例如数据库)中进行抽象。
对于Typescript和Javascript,控制反转(通过Dependency Inversion)意味着通过参数而不是显式导入来嵌入(传递)依赖项。在下面的代码示例中,我们将使用Inversify库,该库允许我们使用装饰器描述依赖项,以便以后创建的类可以动态创建用于解决依赖项的容器。建筑
对于这个简单的应用程序...
我们将创建一个简单的购物车应用程序,该应用程序可以处理向购物车中添加产品的操作。一个购物车可能有多个业务规则,例如每个项目的最小或最大数量。让我们深入研究应用程序层的深度,从最内层开始,然后顺序向外移动。域
根据定义,领域层是业务规则的栖息地。它对任何外部层一无所知,没有依赖性并且可以轻松地进行测试。即使您更改整个应用程序,该域也将保持不变,因为它仅包含可用于理解每个阅读此代码的目的的业务规则。测试应尽可能覆盖这一层。因此,我们有一个Entity基类,它可以包含所有域类共有的某些逻辑。在此示例中,所有通用逻辑都是生成适用于水平缩放的抗碰撞标识符,并且所有对象都将使用此方法。export abstract class Entity<T> {
protected readonly _id: string
protected props: T
constructor(props: T, id?: string) {
this._id = id ? id : UniqueEntityID()
this.props = props
}
}
编写Entity类后,您可以开始创建扩展Entity抽象类的中央域类。在本课程中,没有什么非常复杂的,但是您需要注意一些有趣的地方。首先,构造函数是私有的,这意味着执行新的Cart()会导致错误,这正是我们所需要的。在DDD中,将域对象始终保持在有效状态被认为是一种好习惯。代替直接创建一个空的Cart对象,我们使用Factory模式返回Cart类的完成实例。为确保创建过程已收到所有必需的属性,可以对其进行验证。同样,使用getter和setter与该域进行交互,因此,类的内部状态存储在私有props对象中。获取器提供对应为公共属性的读取访问。同样,公共设置器和其他方法允许您在不断保证对象状态有效性的情况下更改域。总而言之,我们可以强调一个重要的观点:领域逻辑应该专注于行为,而不是属性。export class Cart extends Entity<ICartProps> {
private constructor({ id, ...data }: ICartProps) {
super(data, id)
}
public static create(props: ICartProps): Cart {
const instance = new Cart(props)
return instance
}
public unmarshal(): UnmarshalledCart {
return {
id: this.id,
products: this.products.map(product => ({
item: product.item.unmarshal(),
quantity: product.quantity
})),
totalPrice: this.totalPrice
}
}
private static validQuantity(quantity: number) {
return quantity >= 1 && quantity <= 1000
}
private setProducts(products: CartItem[]) {
this.props.products = products
}
get id(): string {
return this._id
}
get products(): CartItem[] {
return this.props.products
}
get totalPrice(): number {
const cartSum = (acc: number, product: CartItem) => {
return acc + product.item.price * product.quantity
}
return this.products.reduce(cartSum, 0)
}
public add(item: Item, quantity: number) {
if (!Cart.validQuantity(quantity)) {
throw new ValidationError(
'SKU needs to have a quantity between 1 and 1000'
)
}
const index = this.products.findIndex(
product => product.item.sku === item.sku
)
if (index > -1) {
const product = {
...this.products[index],
quantity: this.products[index].quantity + quantity
}
if (!Cart.validQuantity(product.quantity)) {
throw new ValidationError('SKU exceeded allowed quantity')
}
const products = [
...this.products.slice(0, index),
product,
...this.products.slice(index + 1)
]
return this.setProducts(products)
}
const products = [...this.products, { item, quantity }]
this.setProducts(products)
}
public remove(itemId: string) {
const products = this.products.filter(product => product.item.id !== itemId)
this.setProducts(products)
this.emitCartMutation()
}
public empty() {
this.setProducts([])
}
}
尽管应用程序可以使用域方法来使用我们的Cart对象,但在某些情况下,我们可能需要将其“部署”到一个干净的对象中。例如,以JSON对象的形式保存到数据库或发送到客户端。这可以使用unmarshal()方法实现。为了增加架构的灵活性,域层也可以成为域事件的来源。在这里,可以使用事件搜索方法,并在域实体发生更改时创建事件。用户脚本
在这里,我们将使用从基础结构级别实现的域方法和对象来存储数据。我们使用Inverseify库来实现“管理反转”方法,该方法将基础结构层中的存储库注入此方案中,从而使我们能够使用域方法来操作存储篮,然后将更改保存到数据库中。import { inject, injectable } from 'inversify'
@injectable()
export class CartService {
@inject(TYPES.CartRepository) private repository: CartRepository
private async _getCart(id: string): Promise<Cart> {
try {
const cart = await this.repository.getById(id)
return cart
} catch (e) {
const emptyCart = Cart.create({ id, products: [] })
return this.repository.create(emptyCart)
}
}
public getById(id: string): Promise<Cart> {
return this.repository.getById(id)
}
public async add(cartId: string, item: Item, sku: number): Promise<Cart> {
const cart = await this._getCart(cartId)
cart.add(item, sku)
return this.repository.update(cart)
}
public async remove(cartId: string, itemId: string): Promise<Cart> {
const cart = await this._getCart(cartId)
cart.remove(itemId)
return this.repository.update(cart)
}
}
该层负责应用程序的操作。对这一层的代码更改不会影响域实体或外部依赖项,例如数据库。基础设施
尽管有很多明显的地方,但基础架构层仍与外部系统(例如数据库)交互。要将数据保存到数据库,我使用数据映射器和存储库方法。映射器可以从数据库获取源数据并将其转换为相应的域对象:import { Cart, CartItem } from 'src/domain/cart'
const getProducts = (products: CartItem[]) => {
return products.map(product => ({
item: product.item,
quantity: product.quantity
}))
}
export class CartMapper {
public static toDomain(raw: any): Cart {
return Cart.create({
id: raw.id,
couponCode: raw.couponCode,
products: getProducts(raw.products || [])
})
}
}
存储库本身可能取决于特定数据库的客户端库。例如,从RAM中的存储中,并使用以下方法来管理数据:import { injectable, inject } from 'inversify'
import { Cart } from 'src/domain/cart'
import { CartMapper } from '../mappers/cart'
interface ICartRepository {
getById(id: string): Promise<Cart>
create(cart: Cart): Promise<Cart>
update(cart: Cart): Promise<Cart>
}
@injectable()
export class CartRepository implements ICartRepository {
@inject(TYPES.Database) private _database: MemoryData
async getById(id: string): Promise<Cart> {
const cart = await this._database.cart.getById(id)
if (!cart) {
throw new ResourceNotFound('Cart', { id })
}
return CartMapper.toDomain(cart)
}
async create(cart: Cart): Promise<Cart> {
const dtoCart = cart.unmarshal()
const inserted = await this._database.cart.insert(dtoCart)
return CartMapper.toDomain(inserted)
}
async update(cart: Cart): Promise<Cart> {
const dtoCart = cart.unmarshal()
const updated = await this._database.cart.update(cart.id, dtoCart)
return CartMapper.toDomain(updated)
}
}
结论
有很大的改进空间。该代码是为该直观演示而创建的,分层体系结构中的第一个改进可能是在域层中发布存储库接口声明,并在基础架构层中实现它,我们将在下一次讨论。源代码可在github上找到。