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

时间:2015-01-29 13:54:13

标签: c# inheritance domain-driven-design value-objects

情况

我正在构建一个C#Web应用程序,我想将我的应用程序配置建模为一个显式依赖项,通过服务的构造函数交付,而不是直接在每个类中依赖System.Configuration.ConfigurationManager。这在过去经常咬我,所以我希望依赖是明确的,以便项目的下一个维护者(可能是未来的我)不必猜测我的服务获取其配置设置的位置 - 除此之外它是更多TDD友好。此外,我目前正在阅读Eric Evan's Domain Driven Design,我真的想接受他的DDD方法。

我开始对配置类和相应的值对象进行建模以避免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")); 
        }
    }
} 

问题

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

  • 1)我应该在哪里放置OleDbConnectionString的验证逻辑?我真的想要阻止在无效状态下创建值对象 - 这在日常工作中非常有用:-)
    • 我觉得这是域逻辑应该由类本身拥有,但另一方面构造函数应该尽可能少 - 字符串解析不会太多或者这样可以吗?
    • 我可以创建一个验证器,但我肯定不得不通过构造函数将其交给我,以便能够正确地测试该东西,然后我必须手动连接或使用工厂(我绝对是not using a Service Locator )。最重要的是,现在验证将隐藏在单独的服务中;我不会有时间耦合,因为构造函数需要验证器,但仍然看起来不正确。
  • 2)我想知道制作DDD值对象structs是否合适?它们 - 就像名字所暗示的那样 - 代表一个值,这个值是不可变的。但它们将以验证形式包含业务逻辑
  • 3)使用属性检索连接字符串是否可以?如果字符串的格式无效,它可能会抛出异常。此外,完全有可能将实现从读取xml配置文件更改为查询数据库。
  • 4)欢迎任何其他有关设计的评论!

作为旁注,我已经在使用Code Contracts并且有specify object invariants的方式,但我不知道这是否真的是一个好主意,因为这些合同是选择加入的如果它们处于非活动状态,则不再对不变量进行主动保护。我不确定这一点,为了发展目的,尽早发现错误它可能没什么问题,但对于生产来说似乎没什么问题。

THX!

1 个答案:

答案 0 :(得分:0)

我从未真正考虑将常规设置视为DDD问题 - 您是在建模一个关于设置及其保存方式的域,还是仅允许在具有一些内部部件建模为DDD的应用程序中保存和使用设置?

您可以通过将设置与使用设置的内容分开来解决这个问题。

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

如果无法检索设置,我不认为抛出异常是一个好主意,因此您可以返回允许程序继续运行的默认值。

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

我会考虑允许构造发生,但是在使用服务时,即Sender.Send()Sender.Connect()就是在什么时候抛出异常。

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

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

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注册所有应用程序接口(因此我可以基于任何应用程序接口进行查询并返回设置类对象):

resolver.RegisterAsImplementedInterfaces<ApplicationSettings>();

然后我可以注入特定的接口,例如是一个使用IRepository的命令处理程序,而后者依次将EventStoreRepository(作为IRepository的实现连接起来)使用IEventStoreSettings(它连接为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>>();连接到我的所有设置,一直到实现水平。