领域模型验证、继承和可测试性

Domain model validation, inheritance and testability

情况

我正在构建一个 C# Web 应用程序,我想将我的应用程序配置建模为通过服务的构造函数提交的显式依赖项,而不是直接依赖 System.Configuration.ConfigurationManager 在每个 classes。这在过去确实经常咬我,所以我希望依赖关系是明确的,以便项目的下一个维护者(可能是未来的我)不必猜测我的服务从哪里获得他们的配置设置 - 最重要的是更多 TDD friendly. Furthermore I'm currently reading Eric Evan's Domain Driven Design and I really want to embrace his DDD 方法。

我开始对配置 class 和相应的值对象建模以避免 Primitive Obsession 但我在途中遇到了一些障碍,我不确定如何适当地处理它们。这是我目前的做法:

// Role interface that can be requested via constructor injection
interface IAppConnectionStringsConfig
{
    OleDbConnectionString AuthenticationConnectionString { get; }
}

// A base class for handling common functionality like
// parsing comma separated lists or default values
class abstract AppConfigBase
{
    protected string GetStringAppSetting(string key) 
    {
        // Get the appropriate string or a default value from
        // System.Configuration.ConfigurationManager
        return theSettingFromSomeConfigSource;
    }
}

// A value object for OLEDB connection strings that also has a
// convenient implicit conversion to string
class OleDbConnectionString
{
    public readonly string Value;

    public OleDbConnectionString(string connectionString)
    {
        Contract.Requires(connectionString != null);
        this.VerifyStructure(connectionString);
        this.Value = connectionString;
    }

    private void VerifyStructure(string text)
    {
        Contract.Requires(text != null);
        // Verify that the given string fulfills the special
        // needs of an OleDbConnectionString (including Provider=...)
        if (!/* isValidOleDbConnectionString */)
        {
            throw new FormatException();
        }
    }

    public implicit operator string(ConnectionString conn)
    {
        return conn.Value;
    }
}

// The actual app config that implements our role interface
class AppConfig : AppConfigBase, IAppConnectionStringsConfig
{
    public OleDbConnectionString AuthenticationConnectionString 
    { 
        get 
        { 
            return new OleDbConnectionString(this.GetStringAppSetting("authconn")); 
        }
    }
} 

问题

我知道构造函数逻辑应该最少,从构造函数调用虚拟方法不是一个好主意。我的问题如下:

附带说明一下,我已经在使用 Code Contracts and there is a way to specify object invariants 但我不知道这是否真的是个好主意,因为这些合约是选择加入的,并且在它们不活动的情况下不变量不再受到积极保护。我不确定这一点,出于开发目的,及早发现错误可能没问题,但对于生产而言,它似乎已关闭。

谢谢!

我从来没有真正将一般设置视为 DDD 问题 - 您是在建模一个关于设置及其保存方式的域,还是只允许保存设置并在具有某些内部部件建模的应用程序中使用作为 DDD?

您可以通过将获取设置的关注点与使用设置的事物分开来将其分开。

可以使用 属性 来检索连接字符串吗?如果字符串的格式无效,它可能会抛出异常。

我认为在无法检索设置时抛出异常不是一个好主意,因此您可以 return 默认值以允许程序继续。

但还要记住,默认的 returned 值(即密码或网络地址)可能会导致依赖于该设置的东西抛出异常。

我会考虑允许构建正常进行,但是当开始使用该服务时,即 Sender.Send()Sender.Connect() 是您抛出异常的时候。

我应该将 OleDbConnectionString 的验证逻辑放在哪里?我真的很想阻止在无效状态下创建值对象

我创建的对象永远不会 return 无效结果,但它们会 return 默认设置值:

public class ApplicationSettings : IIdentityAppSettings, IEventStoreSettings
{
    /* snip */

    static readonly object KeyLock = new object();

    public byte[] StsSigningKey
    {
        get
        {
            byte[] key = null;

            lock (KeyLock)
            {
                var configManager = WebConfigurationManager.OpenWebConfiguration("/");
                var configElement = configManager.AppSettings.Settings["StsSigningKey"];

                if (configElement == null)
                {
                    key = CryptoRandom.CreateRandomKey(32);
                    configManager.AppSettings.Settings.Add("StsSigningKey", Convert.ToBase64String(key));
                    configManager.Save(ConfigurationSaveMode.Modified); // save to config file
                }
                else
                {
                    key = Convert.FromBase64String(configElement.Value);
                }
            }

            return key;
        }

        /* snip */
    }
}

我通常做的事情

我为域模型中定义的每个限界上下文设置接口作为基础设施的一部分 - 这允许我可以参考和信任的许多已知接口提供某种形式的设置。

ApplicationSettings 是在托管我的有界上下文的代码中定义的,无论是控制台应用程序、WebAPI 还是 MVC 等,我可能在同一进程下托管多个有界上下文,或者可以拆分它们作为单独的进程输出,无论哪种方式,托管应用程序的工作都是提供相关的应用程序设置,并且可以通过 IoC 容器进行连接。

public class ApplicationSettings : IIdentityAppSettings, IEventStoreSettings
{
    // implement interfaces here
}

public interface IEventStoreSettings
{
    string EventStoreUsername { get; }
    string EventStorePassword { get; }
    string EventStoreAddress { get; }
    int EventStorePort { get; }
}

public interface IIdentityAppSettings
{
    byte[] StsSigningKey { get; }
}

我使用 SimpleInjector .NET IoC 容器连接我的应用程序。然后,我使用 SimpleInjector 注册所有应用程序接口(这样我就可以根据任何应用程序接口进行查询,并设置 class 对象 returned):

resolver.RegisterAsImplementedInterfaces<ApplicationSettings>();

然后我可以注入特定的接口,一个例子是使用 IRepository 的命令处理程序,而 EventStoreRepository(作为 IRepository 的一个实现)使用 IEventStoreSettings(作为 IRepository 的一个实现) ApplicationSettings 实例):

public class HandleUserStats : ICommandHandler<UserStats>
{
    protected IRepository repository;

    public HandleUserStats(IRepository repository)
    {
        this.repository = repository;
    }

    public void Handle(UserStats stats)
    {
        // do something
    }
}

然后我的存储库将被连接起来:

public class EventStoreRepository : IRepository
{
    IEventStoreSettings eventStoreSettings;

    public EventStoreRepository(IEventStoreSettings eventStoreSettings)
    {
        this.eventStoreSettings = eventStoreSettings;
    }

    public void Write(object obj)
    {
        // just some mockup code to show how to access setting
        var eventStoreClient = new EventStoreClient(
                                        this.eventStoreSettings.EventStoreUsername,
                                        this.eventStoreSettings.EventStorePassword,
                                        this.eventStoreSettings.EventStoreAddress,
                                        this.eventStoreSettings.Port
                                        ); 

        // if ever there was an exception either during setup of the connection, or
        // exception (if you don't return a default value) accessing settings, it
        // could be caught and bubbled up as an InfrastructureException

        // now do something with the event store! ....
    }
}

我允许从某些外部源(如 WCF 接收或 MVC 控制器操作)传入设置,并通过获取 resolver.GetInstance<CommandHandler<UserStats>>(); 进行连接,这为我连接了所有设置,一直到执行级别。