让我们通过介绍一个众所周知的模式 — 工厂来继续领域驱动设计的故事。
一些软件开发人员提供一堆超快的代码,破坏了scrum 看板,周三开始为期两周的冲刺,到周五早上他们已经完成了所有任务。
我见过很多次这种情况,每个团队都需要至少一个这样的人,能够快速完成任务。不幸的是,许多人在代码质量上挣扎,这有时会隐藏潜在的错误。
我知道,我就是那些“罪犯”之一。提供大量的代码,但质量很差。逻辑散布在许多地方,异常的类和方法,代码难以进行单元测试,并且不断出现错误。
幸运的是,有一年夏天我花了很长时间阅读了一本可能在我的职业生涯中最重要的书——Clean Code。结果是,即使在5年之后,同样的编码速度,但生产代码仍然没有任何错误。
我非常重视的一点是工厂的使用。你们中的许多人从传奇模式如工厂方法或抽象工厂中知道它。伟大的事情是DDD也采用了这样的模式。
没有什么比大型构造函数更让我烦恼的了,很明显,它们提出了一些业务逻辑,但是说真的,我真的想对构造函数进行单元测试吗?
工厂模式的派生在领域驱动设计中是必不可少的,并且,即使在几十年后,其目的仍然是相同的。
复杂的作品
我们使用工厂模式来处理任何复杂的创建或将创建过程从其他业务逻辑中隔离出来。这样的创建逻辑可能非常长,并且有很多对业务至关重要的用例。
在这种情况下,最好在代码中有一个专门的位置,我们可以单独测试。
工厂,在大多数情况下,是域层的一部分。从那里,我们可以在应用程序的任何地方使用它。下面你可以看到一个工厂的简单示例。
LoanFactory
class Loan {
constructor(
private id: string,
//
// some fields
//
) {}
}
interface LoanFactory {
createShortTermLoan(specification: LoanSpecification): Loan;
createLongTermLoan(specification: LoanSpecification): Loan;
}
工厂模式与规范模式是紧密相关的。这里我们有一个小例子,其中 LoanFactory
, LoanSpecification
和 Loan
作为一个实体。
LoanFactory
代表 DDD 中的工厂模式,更确切地说,代表工厂方法。它负责创建和返回 Loan
的新实例,这些实例可以根据支付周期而变化。
变化
我们可以用很多不同的方式来表示工厂模式。至少对我来说,最常见的形式是工厂方法。在这种情况下,我们为工厂结构提供了一些创建方法。
工厂方法
class Loan {
constructor(
private months: number,
private bankAccountID: string,
private amount: Money,
private requiredLifeInsurance: boolean,
) {}
}
class LoanFactory {
public createShortTermLoan(bankAccountID: string, amount: Money): Loan {
return new Loan(
12,
bankAccountID,
amount,
false,
);
}
createLongTermLoan(bankAccountID: string, amount: Money): Loan {
return new Loan(
360,
bankAccountID,
amount,
true
);
}
}
在上面的代码片段中,我们可以看到工厂方法的具体实现,它提供了两个方法来创建 Loan
实体的实例。
在这种情况下,我们创建了相同的对象,但它可以有差异,这取决于 Loan
是长期的还是短期的,两种情况之间的差异可以更加复杂,而每个额外的复杂性都是该模式存在的新原因。
现在让我们检查下面的示例,一个使用抽象工厂模式的代码片段,在这个例子中,我们想要创建一些 Investment
接口的实例。
抽象工厂
interface Investment {
amount(): Money;
}
class EtfInvestment {
constructor(
private etfId: string,
private investedAmount: Money,
private bankAccountId: string
) {}
amount(): Money {
return this.investedAmount;
}
}
class StockInvestment {
constructor(
private companyId: string,
private investedAmount: Money,
private bankAccountId: string
) {}
amount(): Money {
return this.investedAmount;
}
}
interface InvestmentSpecification {
amount(): Money
bankAccountId(): string;
targetId(): string;
}
interface InvestmentFactory {
create(specification: InvestmentSpecification): Investment;
}
class EtfInvestmentFactory {
create(specification: InvestmentSpecification): Investment {
const investment = new EtfInvestment(
specification.targetId(),
specification.amount(),
specification.bankAccountId()
);
return investment;
}
}
class StockInvestmentFactory {
create(specification: InvestmentSpecification): Investment {
const investment = new StockInvestment(
specification.targetId(),
specification.amount(),
specification.bankAccountId()
);
return investment;
}
}
由于 Investment
接口有多个实现,这看起来是添加 Factory 模式的完美时机,因为 EtfInvestmentFactory
和 StockInvestmentFactory
都创建了该接口的实例。
在我们的代码中,我们可以将它们保存在某个 InvestmentFactory
接口的映射中,并在需要从任何 BankAccount
创建 Investment
时使用它们。
重建
我们可以在其他层上使用工厂模式,比如基础设施和表示层。在那里,我们应该使用它来将数据访问对象或数据传输对象转换为实体,反之亦然。
这些情况非常有用,因为我们可以为我们的逻辑提供隔离,以将外部提供的数据映射到该数据的内部表示。在那里,我们可以自由地提供我们想要的任意数量的单元测试。
我们应该提供这样的测试,因为这个逻辑对于拥有一个功能性的反腐败层来说是至关重要的。
让我们看下面的例子。
重构DTO到实体
class CryptoInvestmentDB {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'uuid' })
uuid: string;
@Column({ name: 'crypto_currency_id' })
cryptoCurrencyId: number;
@Column({ name: 'invested_amount' })
investedAmount: number;
@Column({ name: 'invested_amount' })
investedCurrencyId: number;
@Column({ name: 'invested_amount' })
bankAccountId: number;
//
// some fields
//
}
class CryptoInvestmentDBFactory {
toEntity(dto: CryptoInvestmentDB): CryptoInvestment {
uuidParse.parse(dto.uuid);
uuidParse.parse(dto.cryptoCurrency.uuid);
uuidParse.parse(dto.currency.uuid);
uuidParse.parse(dto.bankAccount.uuid);
return CryptoInvestment{
id: id,
cryptoCurrencyID: dto.cryptoCurrency.uuid,
investedAmount: new Money(dto.investedAmount, currencyId),
BankAccountID: accountId,
};
}
}
CryptoInvestmentDBFactory
是基础架构层中的一个工厂,用于重构 CryptoInvestment 实体。这里只有一个将 DTO 转换为 Entity 的方法,但同一个 Factory 可以有一个将 Entity 转换为 DTO 的方法。
因为CryptoInvestmentDBFactory
同时使用了来自基础架构层 ( CryptoInvestmentGorm
) 和域层 ( CryptoInvestment
) 的类。
映射的整个代码必须在基础设施层内,因为我们不能在域层内对其他层有任何依赖。
结论
工厂模式是一个概念,其根源在于“四人组”的旧模式。我们可以将其实现为抽象工厂或工厂方法。
当我们想将创建逻辑从其他业务逻辑中解耦时,我们会使用它。我们还可以使用它将我们的实体转换为DAO或DTO,反之亦然。
TypeScript领域驱动设计(DDD)系列:
1.
TypeScript中的实用领域驱动设计(DDD):为什么重要?
3.
在TypeScript中实践DDD(领域驱动设计):实体
4.
在TypeScript中实践DDD(领域驱动设计):域服务
欢迎关注公众号:文本魔术,了解更多