【问题标题】:DDD - Behavior with optional state: Service / Value Object?DDD - 具有可选状态的行为:服务/值对象?
【发布时间】:2017-02-28 19:43:52
【问题描述】:

按照 DDD 实践,我在实现小型 AES 加密器/解密器(包装 .NET 的 AesCryptoServiceProvider)时遇到了问题。

public class Aes256CbcCryptor : ISymmetricCryptor
{
    private SymmetricAlgorithm AesProvider { get; set; }

    // Poor man's DI - beside the point
    public Aes256Cbc()
    {
        this.AesProvider = new AesCryptoServiceProvider()
        {
            BlockSize = 128,
            KeySize = 256,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
            Key = // OH DEAR IT TAKES STATE
        };
    }
    public Aes256Cbc(SymmetricAlgorithm aesProvider)
    {
        this.AesProvider = aesProvider;
    }

    public byte[] Encrypt(byte[] keyBytes, byte[] plaintextBytes)
    {} // TODO
    public byte[] Decrypt(byte[] keyBytes, byte[] ciphertextBytes)
    {} // TODO
}

如您所见,.NET 的AesCryptoServiceProvider 是有状态的——它需要一个键作为属性。但据我了解,服务不应该是有状态的。

  1. 这里的主要问题是关键是属性(而不是方法参数)吗?
  2. 您将如何以 DDD 方式实现该类?
  3. 在某些情况下,使用给定密钥初始化提供程序似乎有用且高效(如果该密钥被大量使用)。有状态服务是否有理由或替代方案?

我想我们可以在每个方法调用上都实例化一个新的 Provider,但这似乎非常浪费。我们可以实施缓存来减少浪费,但整个事情开始感觉过度设计。

我想出的另一种选择是创建一个Aes256CbcCryptorFactory。工厂的CreateCryptor(byte[] key) 返回一个实际上是有状态的值对象Aes256CbcCryptor。如果需要进行多次加密调用,消费服务现在至少可以将此对象保留在其方法之一的范围内。

另一方面,这样的消费服务仍然不能将值对象存储在它的一个属性中,因为这样做会使该服务成为有状态的。

  1. 既然有一些好处,这件事已经完成了吗?对于值对象,这种行为类型似乎非常服务,但至少我们可以有一些临时状态。

【问题讨论】:

  • 你用“穷人的 DI”评论了你的构造函数。这里没有 DI:您使用 new 关键字来创建实例。也许这是你的问题。你不应该让AesCryptoServiceProvider 成为一个依赖项,然后让你的组合根担心状态吗?
  • @DavidOsborne 我省略了参数化构造函数,但现在添加了它。无参数构造函数是“穷人的 DI”,即框架自动注入的替代方案。不过,我对你的观点很感兴趣。组合根知道使用什么键是有道理的。该根将是服务本身,对吗?但是我们不需要防止 it(和 this)变成有状态的吗?
  • “穷人的 DI”是没有容器的 DI (blog.ploeh.dk/2014/06/10/pure-di/)。组合根实际上是什么取决于实现。您似乎无法逃避您选择的实现需要状态的事实。您可以进一步向上/向外推动该状态的解析,但在某些时候您需要管理该状态。
  • @DavidOsborne 同意。我正在寻找管理它的正确方法/地点,据我了解,这不在服务中。有什么建议吗?
  • @Timo 为什么你认为这与 DDD 有关?

标签: c# domain-driven-design stateful value-objects ddd-service


【解决方案1】:

我会选择这样的:

public class Aes256CbcCryptor : ISymmetricCryptor
{
    private SymmetricAlgorithm AesProvider { get; }

    public Aes256CbcCryptor(Byte[] key)
    {
        // AesCryptoServiceProvider is not a 'volatile' dependency
        // therefore we don't need to inject it.
        this.AesProvider = 
            new AesCryptoServiceProvider()
            {
                ...
                Key = key// This is the real dependency, IMHO
            };
    }
}

然后组合根,使用“穷人的 DI”会这样:

public static sub Main()
{
    var key = SomeConfigSomewhere.GetSetting["key"];

    var cryptor = new Aes256CbcCryptor(key);

    var cipherText = cryptor.Encrypt("P@55w0rd");
}

使用容器会使解析更简洁,但本质上是相同的:

public static sub Main()
{
    arbitraryContainer
        .Register
        .ServiceFor<ISymmetricCryptor>()
        .Using<Aes256CbcCryptor>()
        .DependingOn(SomeConfigSomewhere.GetSetting["key"]);

    var cryptor = arbitraryContainer.Resolve<ISymmetricCryptor>();

    var cipherText = cryptor.Encrypt("P@55w0rd");
}

【讨论】:

  • 当我们向上移动抽象层时,我仍然看到一些问题。对于这个 Cryptor,密钥作为构造函数依赖可能是有意义的,因为它几乎所有的方法都需要它。现在图像有一个 CardNumberCryptor 服务,它具有 Cryptor 作为依赖项。它又被另一个服务使用,而另一个服务又被 RecurringCardPayment 服务使用。现在,我无法在没有密钥的情况下实例化/测试 RecurringCardPayment 的方法,这可能只与它的 one 方法有关。
  • CardNumberCryptor 应该依赖于ISymmetricCryptor。底层实现不是它所关心的。
  • 啊,CardNumberCryptor 要么传递了一些 ISymmetricCryptor,要么容器执行此操作。对于容器,我猜容器需要知道要使用什么键,这意味着容器与(在此示例中)我们的 Main() 方法紧密相关 - 就像在上一个示例中一样。这是正常的状态吗?它看起来很……精致。
  • 在本例中,Main() 是组合根。这是应该配置容器的地方。当对容器的 [直接] 引用开始从组合根中泄漏出来时,您就使用了 ServiceLocator 反模式。我想这有点复杂,但我想这是解耦组件的潜在成本。
  • 如果可以的话,我强烈推荐阅读 Mark Seemann 的书 (amazon.co.uk/dp/1935182501)。虽然它的标题非常集中,但它涵盖了很多关于 OO 设计和 DI 在其中的角色的基础。
猜你喜欢
  • 1970-01-01
  • 2013-02-01
  • 2016-02-24
  • 2013-10-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多