【问题标题】:Domain model validation, inheritance and testability领域模型验证、继承和可测试性
【发布时间】:2015-01-29 13:54:12
【问题描述】:

情况

我正在构建一个 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 值对象@​​987654331@?它们 - 顾名思义 - 代表单个值,并且该值是不可变的。但它们会以验证的形式包含业务逻辑
  • 3) 是否可以使用属性来检索连接字符串?如果字符串的格式无效,它可能会引发异常。此外,完全有可能将实现从读取 xml 配置文件更改为查询数据库。
  • 4) 欢迎设计中的任何其他 cmets!

附带说明一下,我已经在使用 Code Contracts 并且有一种方法可以使用 specify object invariants 但我不知道这是否真的是一个好主意,因为这些合同是可选的,并且在这种情况下它们是不活动的,不变量不再受到积极保护。我不确定这一点,出于开发目的及早发现错误可能没问题,但对于生产来说它似乎关闭了。

谢谢!

【问题讨论】:

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


    【解决方案1】:

    我从未真正将常规设置视为 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&lt;CommandHandler&lt;UserStats&gt;&gt;(); 将所有设置连接到实现级别。

    【讨论】:

    • 这种方法有效,但最终构造函数/getter 异常是我想要的行为,一旦出现问题,它就会爆炸,并且在我的项目中运行了一段时间。跨度>
    猜你喜欢
    • 2014-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-09
    • 1970-01-01
    • 2012-09-29
    • 2011-09-11
    • 2012-08-07
    相关资源
    最近更新 更多