你的JavaScript代码变成意大利面了?是时候引入DDD了
\n\n你是否曾经发现JavaScript项目变得像一盘意大利面?方法相互纠缠,一个模块的改动破坏了另外三个,业务逻辑分散在控制器、服务和工具函数中。熟悉吗?如果你的项目已经超越了“只是一个原型”的阶段,那么是时候了解领域驱动设计(DDD)了。
\n\n许多人认为DDD是Java或C#巨头的专利。但这是一个误解。JavaScript项目中的领域驱动设计不仅可行,而且非常有用。它帮助开发者和业务使用同一种语言沟通,代码变得可预测且易于变更。在本文中,我们将通过实际代码示例来实践DDD,以便你今天就能应用它。
\n\n什么是DDD,为什么JavaScript开发者需要它?
\n\nDDD不是库也不是框架。它是一种软件设计方法,将业务领域置于核心。与其考虑数据库表或REST API,不如思考业务实体:“订单”、“客户”、“发票”、“折扣计算规则”。
\n\nDDD解决的主要痛点是复杂性。当业务逻辑在一个地方,而技术细节在另一个地方时,项目变得脆弱。DDD提供了清晰的架构,将核心与细节分离。
\n\n对于JavaScript,我们经常使用动态类型和原型,DDD带来了纪律。它强制显式描述数据类型及其变更规则,这对于Node.js上的可扩展应用或复杂的客户端应用至关重要。
\n\nDDD的主要构建块(附JS示例)
\n\n让我们进入最有趣的部分——代码。我们将编写一个简单但具有代表性的电商订单管理系统。我们将使用现代JavaScript(ES6+)。
\n\n1. 值对象(Value Object)
\n\n这是一个不可变对象,由其属性定义,而非身份。两个值对象如果值相同则相等。完美的例子是金钱、地址、电子邮件。
\n\n// value-objects/Money.js\nclass Money {\n constructor(amount, currency) {\n // 验证是DDD的关键部分\n if (typeof amount !== 'number' || amount < 0) {\n throw new Error('金额必须为正数');\n }\n if (!['USD', 'RUB', 'EUR'].includes(currency)) {\n throw new Error('不支持的货币');\n }\n\n this._amount = amount;\n this._currency = currency;\n // 冻结对象以实现不可变性\n Object.freeze(this);\n }\n\n get amount() { return this._amount; }\n get currency() { return this._currency; }\n\n // 业务方法:货币加法\n add(otherMoney) {\n if (this.currency !== otherMoney.currency) {\n throw new Error('不能相加不同货币');\n }\n return new Money(this.amount + otherMoney.amount, this.currency);\n }\n\n equals(other) {\n return this.amount === other.amount && this.currency === other.currency;\n }\n}\n\n// 使用\nconst price = new Money(100, 'USD');\nconst tax = new Money(20, 'USD');\nconst total = price.add(tax); // Money { _amount: 120, _currency: 'USD' }\nconsole.log(total);\n\n为什么这很酷?现在金额不仅仅是一个数字,而是一个带有规则的完整实体。你不会意外地将美元与卢布相加,或使金额为负。
\n\n2. 实体(Entity)
\n\n与值对象不同,实体具有身份。它可以随时间变化,但其ID保持不变。例如,用户或订单。
\n\n// entities/Order.js\nconst { v4: uuidv4 } = require('uuid'); // 或 crypto.randomUUID()\n\nclass Order {\n constructor(id