DDD 聚合和实体中的 InversionOfControl(依赖注入)

Ste*_*o L 5 domain-driven-design inversion-of-control

我的问题是关于你们在实践中如何将域或基础设施服务注入到 DDD 聚合中。我确信在 DomainObjects 中允许 DependencyInjection通常是一个坏主意,因为根据我的经验,它鼓励鲁莽的开发人员用它做令人讨厌的事情。但是可以肯定的是,总是有例外情况,Domainobjects 中的 DependencyInjection 可能有意义,特别是当它有利于可读性和简单性时。就我而言,我正在尝试解决如何创建新Ids的 anaggregate的问题root

让我们假设我们有一个UserAddressesAddress实体列表作为其聚合根的聚合。让我们进一步假设我们changeAddress(AddressId addressId, AddressChangeDto dto)在该聚合中有一个方法:

public AddressId changeAddress(AddressId addressId, AddressChangeDto dto) {
    Address address = nullableAddress(addressId);

    if (address == null) {
        // Doesn't matter for this question
    } else if (address.hasChanged(dto)) {
        address = changeAddress(address, dto);            
    }
}
Run Code Online (Sandbox Code Playgroud)

和一个私有changeAddress(Address address, AddressChangeDto dto)方法简化如下:

private Address changeAddress(Address address, AddressChangeDto dto) {
    if (!addressCopyNeeded(address)) {
        address.change(dto);
        return address;
    }

    // Address is both Shipping- & BillingAddress, hence it must be copied to 2 separate
    // entities.
    Address copiedAddress = address.copy(new AddressId()); // <-- New AddressId created
    ...
}
Run Code Online (Sandbox Code Playgroud)

现在我想重构AddressId要从AddressIdFactory DomainService驻留在我的 DomainModel 中创建的创建,该创建由驻留在基础设施中的 MongoDbRepository 支持,以创建ObjectId从 MongoDb优化的性能,我更喜欢长 UUID。

可以通过将该依赖项添加为形式参数来轻松解决这个问题,如下所示:

public AddressId changeAddress(AddressId addressId, AddressChangeDto dto, AddressIdFactory idFactory) {
    Address address = nullableAddress(addressId);

    if (address == null) {
        // Doesn't matter for this question
    } else if (address.hasChanged(dto)) {
        address = changeAddress(address, dto, idFactory);            
    }
}

private Address changeAddress(Address address, AddressChangeDto dto, AddressIdFactory idFactory) {
    if (!addressCopyNeeded(address)) {
        address.change(dto);
        return address;
    }

    // Address is both Shipping- & BillingAddress, hence it must be copied to 2 separate
    // entities.
    Address copiedAddress = address.copy(idFactory.nextAddressId());
    ...
}
Run Code Online (Sandbox Code Playgroud)

但是,我真的一点也不喜欢这种设计,因为它显然让客户第一眼看到这种方法时感到困惑:“为什么AddressIdFactory我要更改现有地址时必须通过?” 客户端没有,也不应该知道内部发生了什么,并且有可能必须在某个星座中创建一个新地址。这也导致了我的下一个论点,我们也可以重构您可以说的整个方法,但是这将始终导致客户端负责传入新的 AddressId 或传入 xyz DomainService 作为方法参数的设计,其中我认为这根本不是最佳选择。

正因为如此,我正在考虑将依赖注入作为一个例外情况,以便能够保持“何时”和“如何”的逻辑在聚合中创建一个新地址,以方便聚合的客户.

问题是如何。是的,我们正在使用 Spring,不,我不能只使用自动装配 DomainService,因为整个聚合是一个持久化的 Spring Data MongoDb 对象,其中 DomainObject 在运行时从 JSON 反序列化。

@Aggregate
@Document(collection = "useraddresses)
public class UserAddresses extends Entity {

    // When reading from MongoDb the object's creation lays in the responsibility of 
    // Spring Data MongoDb, not Spring, therefore Spring cannot inject the dependency 
    // at all.         
    @Autowired
    private AddressIdFactory addressIdFactory;

    @PersistenceConstructor
    public UserAddresses(String id, Map<String, Address> userAddresses) {
    setUserAddressesId(new UserAddressesId(id));

    if (userAddresses != null) {
        this.userAddresses.putAll(userAddresses);
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,我可以通过调用 setter() 或其他东西在存储库中手动注入该依赖项,但是这也很丑陋,因为我只会出于技术问题的目的在我的“美丽”聚合上使用公共 setter。

另一个想法是使用反射将依赖项直接设置为私有字段,但是我猜这也很丑陋并且有轻微的性能缺陷。

我想知道你的方法会是什么样子?

Voi*_*son 3

因此,解决这个问题的方法是“控制反转”,这实际上只是“进行争论”的一种自命不凡的说法。

我不怪你根本不喜欢这个设计;确实感觉模型的实现细节正在泄露给应用程序。

这个谜题的部分答案是这样的:您可以通过接口将应用程序与实现的细节隔离开来。

interface UserAddresses {
    AddressId changeAddress(AddressId addressId, AddressChangeDto dto);
}
Run Code Online (Sandbox Code Playgroud)

因此,当应用程序从存储库加载“聚合”时,它会获得实现此接口的东西。

{
    UserAddresses root = repository.get(...)
    root.changeAddress(addressId, dto)
}
Run Code Online (Sandbox Code Playgroud)

应用程序不必直接与聚合的“根实体”对话;应用程序可能正在与适配器通信。

Adapter::changeAddress(AddressId addressId, AddressChangeDto dto) {
    this.target.changeAddress(addressId, dto, this.addressFactory);
}
Run Code Online (Sandbox Code Playgroud)

同样,应用程序正在与之通信的“”存储库是围绕存储库访问数据存储的适配器,以及将地址工厂注入适配器的附加管道。