【问题标题】:How can I improve this design?我该如何改进这个设计?
【发布时间】:2011-01-27 18:45:00
【问题描述】:

假设我们的系统可以执行动作,并且动作需要一些参数来完成它的工作。 我为所有动作定义了以下基类(为了您的阅读乐趣而进行了简化):

public abstract class BaseBusinessAction<TActionParameters> 
    : where TActionParameters : IActionParameters
{
    protected BaseBusinessAction(TActionParameters actionParameters)
    {
        if (actionParameters == null) 
            throw new ArgumentNullException("actionParameters"); 

        this.Parameters = actionParameters;

        if (!ParametersAreValid()) 
            throw new ArgumentException("Valid parameters must be supplied", "actionParameters");
    }

    protected TActionParameters Parameters { get; private set; }

    protected abstract bool ParametersAreValid();

    public void CommonMethod() { ... }
}

只有BaseBusinessAction 的具体实现知道如何验证传递给它的参数是否有效,因此 ParametersAreValid 是一个抽象函数。但是,我希望基类构造函数强制传递的参数始终有效,所以我添加了一个 对构造函数调用ParametersAreValid,当函数返回false 时抛出异常。到目前为止一切顺利,对吧?嗯,不。 代码分析告诉我“not call overridable methods in constructors”这实际上很有意义,因为当基类的构造函数被调用时 子类的构造函数尚未被调用,因此ParametersAreValid 方法可能无法访问某些关键成员变量 子类的构造函数会设置。

所以问题是:我该如何改进这个设计?

我是否将Func&lt;bool, TActionParameters&gt; 参数添加到基类构造函数?如果我这样做了:

public class MyAction<MyParameters>
{
    public MyAction(MyParameters actionParameters, bool something) : base(actionParameters, ValidateIt)
    {
        this.something = something;
    }

    private bool something;

    public static bool ValidateIt()
    {
       return something;
    }

}

这会起作用,因为ValidateIt 是静态的,但我不知道...有没有更好的方法?

欢迎评论。

【问题讨论】:

  • +1,你让我写代码是为了实验。
  • ValidateIt 方法是否需要访问 MyParameters 私有数据才能对其进行验证?也许您应该将 MyParameters 实例传递给 ValidateIt。或者,MyParameters 可以验证自己吗?或者,您计划从 Action 基类实现的功能可以通过拥有对操作的引用而不是对它们进行超类型化来更好地实现?

标签: c# .net generics oop


【解决方案1】:

这是继承层次结构中常见的设计挑战——如何在构造函数中执行依赖于类的行为。代码分析工具将此标记为问题的原因是派生类的构造函数此时还没有机会运行,并且对虚方法的调用可能依赖于尚未初始化的状态。

所以你在这里有几个选择:

  1. 忽略这个问题。如果您认为实现者应该能够编写参数验证方法而不依赖于类的任何运行时状态,那么请记录该假设并坚持您的设计。
  2. 将验证逻辑移动到每个派生类构造函数中,让基类只执行它必须执行的最基本的抽象类型的验证(空检查等)。
  3. 在每个派生类中复制逻辑。这种代码重复似乎令人不安,它为派生类打开了忘记执行必要设置或验证逻辑的大门。
  4. 提供某种必须由消费者(或您的类型的工厂)调用的 Initialize() 方法,以确保在完全构造类型后执行此验证。这可能是不可取的,因为它要求任何实例化您的类的人都必须记住调用初始化方法——您认为构造函数可以执行该方法。通常,工厂可以帮助避免这个问题 - 它是唯一允许实例化您的类的工厂,并且会在将类型返回给消费者之前调用初始化逻辑。
  5. 如果验证不依赖于状态,然后将验证器分解为一个单独的类型,您甚至可以将其作为泛型类签名的一部分。然后,您可以在构造函数中实例化验证器,将参数传递给它。每个派生类都可以定义一个带有默认构造函数的嵌套类,并将所有参数验证逻辑放在那里。下面提供了此模式的代码示例。

如果可能,让每个构造函数执行验证。但这并不总是可取的。在这种情况下,我个人更喜欢工厂模式,因为它使实现保持直截了当,并且它还提供了一个拦截点,以后可以添加其他行为(日志记录、缓存等)。但是,有时工厂没有意义,在这种情况下,我会认真考虑创建独立验证器类型的第四个选项。

代码示例如下:

public interface IParamValidator<TParams> 
    where TParams : IActionParameters
{
    bool ValidateParameters( TParams parameters );
}

public abstract class BaseBusinessAction<TActionParameters,TParamValidator> 
    where TActionParameters : IActionParameters
    where TParamValidator : IParamValidator<TActionParameters>, new()
{
    protected BaseBusinessAction(TActionParameters actionParameters)
    {
        if (actionParameters == null) 
            throw new ArgumentNullException("actionParameters"); 

        // delegate detailed validation to the supplied IParamValidator
        var paramValidator = new TParamValidator();
        // you may want to implement the throw inside the Validator 
        // so additional detail can be added...
        if( !paramValidator.ValidateParameters( actionParameters ) )
            throw new ArgumentException("Valid parameters must be supplied", "actionParameters");

        this.Parameters = actionParameters;
    }
 }

public class MyAction : BaseBusinessAction<MyActionParams,MyActionValidator>
{
    // nested validator class
    private class MyActionValidator : IParamValidator<MyActionParams>
    {
        public MyActionValidator() {} // default constructor 
        // implement appropriate validation logic
        public bool ValidateParameters( MyActionParams params ) { return true; /*...*/ }
    }
}

【讨论】:

  • 我喜欢你关于让工厂调用 init 方法的观点。我已经有一个可以做到这一点的工厂类。
  • 是的,我想我更喜欢这个,因为它使验证更加明确,而不是像其他一些海报所建议的那样仅仅依靠子类来验证参数。
【解决方案2】:

如果您无论如何都推迟到子类来验证参数,为什么不在子类构造函数中简单地这样做呢?我理解您正在努力争取的原则,即强制从您的基类派生的任何类都验证其参数。但即便如此,您的基类的用户也可以简单地实现一个简单地返回 true 的 ParametersAreValid() 版本,在这种情况下,该类遵守了合同的文字,而不是精神。

对我来说,我通常将这种验证放在调用任何方法的开头。例如,

public MyAction(MyParameters actionParameters, bool something)
    : base(actionParameters) 
{ 
    #region Pre-Conditions
        if (actionParameters == null) throw new ArgumentNullException();
        // Perform additional validation here...
    #endregion Pre-Conditions

    this.something = something; 
} 

我希望这会有所帮助。

【讨论】:

  • +1 这正是我的想法。子类甚至可能不需要任何特殊验证,在这种情况下,虚拟调用毫无意义。只有派生类的设计者知道它的需求是什么。这很好地避免了在构造函数中调用虚方法。
【解决方案3】:

我建议将Single Responsibility Principle 应用于该问题。看来Action类应该负责一件事;执行动作。鉴于此,应将验证移至仅负责验证的单独对象。你可以使用一些通用接口来定义验证器:

IParameterValidator<TActionParameters>
{
    Validate(TActionParameters parameters);
}

然后您可以将其添加到您的基本构造函数中,并在那里调用 validate 方法:

protected BaseBusinessAction(IParameterValidator<TActionParameters> validator, TActionParameters actionParameters)
{
    if (actionParameters == null) 
        throw new ArgumentNullException("actionParameters"); 

    this.Parameters = actionParameters;

    if (!validator.Validate(actionParameters)) 
        throw new ArgumentException("Valid parameters must be supplied", "actionParameters");
}

这种方法有一个很好的隐藏好处,它允许您更轻松地重用跨操作通用的验证规则。如果您使用的是 IoC 容器,那么您还可以轻松添加绑定约定,以根据 TActionParameters 的类型自动适当地绑定 IParameterValidator 实现

【讨论】:

  • +1 感谢您的建议。我使用的是 IoC 容器,您对 SRP 的看法是正确的。
  • 这个解决了编译问题,保持了最初设计的精神/目的。它最终增加了更高的灵活性水平。
【解决方案4】:

我过去遇到过非常类似的问题,我最终将验证参数的逻辑移到了适当的ActionParameters 类中。如果您的参数类与 BusinessAction 类对齐,则此方法可以开箱即用。

如果不是这样,它会变得更加痛苦。您有以下选择(我个人更喜欢第一个):

  1. 将所有参数包装在IValidatableParameters 中。实施将与业务操作对齐并提供验证
  2. 只需取消此警告
  3. 将此检查移至父类,但最终导致代码重复
  4. 将此检查移至实际使用参数的方法(但您的代码稍后会失败)

【讨论】:

    【解决方案5】:

    为什么不这样做:

    public abstract class BaseBusinessAction<TActionParameters> 
        : where TActionParameters : IActionParameters
    {
    
        protected abstract TActionParameters Parameters { get; }
    
        protected abstract bool ParametersAreValid();
    
        public void CommonMethod() { ... }
    }
    

    现在具体类必须担心参数并确保它们的有效性。在执行任何其他操作之前,我只会强制执行 CommonMethod 调用 ParametersAreValid 方法。

    【讨论】:

      【解决方案6】:

      如何将验证移至逻辑中更常见的位置。不要在构造函数中运行验证,而是在第一次(也是第一次)调用该方法时运行它。这样,其他开发人员可以构造对象,然后在执行操作之前更改或修复参数。

      您可以通过更改参数属性的 getter/setter 来做到这一点,因此任何使用参数的东西都会在第一次使用时验证它们。

      【讨论】:

        【解决方案7】:

        预期在哪里使用参数:来自 CommonMethod?不清楚为什么参数必须在实例化时而不是在使用时有效,因此您可能会选择让派生类在使用前验证参数。

        编辑 - 据我所知,问题似乎是构建类所需的特殊工作之一。对我来说,这就是用于构建 BaseBusinessAction 实例的 Factory 类,其中它会在构建时调用它所构建的实例上的虚拟 Validate()。

        【讨论】:

        • CommonMethod 仅用于说明目的。实际上有很多常用方法都依赖于参数是否有效。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多