我该如何改进这个设计?

时间:2010-03-15 16:56:43

标签: c# .net generics oop

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

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<bool, TActionParameters>参数?如果我这样做了:

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是静态的,但我不知道......有更好的方法吗?

非常欢迎评论。

7 个答案:

答案 0 :(得分:7)

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

所以你在这里有一些选择:

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

    以下是代码示例:

    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; /*...*/ }
        }
    }
    

答案 1 :(得分:5)

如果您正在推迟子类以验证参数,为什么不在子类构造函数中简单地执行此操作?我理解您正在努力的原则,即强制执行从您的基类派生的任何类验证其参数。但即使这样,基类的用户也可以简单地实现一个只返回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; 
} 

我希望这会有所帮助。

答案 2 :(得分: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实现

答案 3 :(得分:1)

我过去遇到了一个非常类似的问题,最后我将逻辑移动到适当的ActionParameters类来验证参数。如果您的参数类与BusinessAction类对齐,则此方法可以立即使用。

如果情况并非如此,则会更加痛苦。你有以下选择(我更喜欢第一个个人):

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

答案 4 :(得分:0)

为什么不这样做:

public abstract class BaseBusinessAction<TActionParameters> 
    : where TActionParameters : IActionParameters
{

    protected abstract TActionParameters Parameters { get; }

    protected abstract bool ParametersAreValid();

    public void CommonMethod() { ... }
}

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

答案 5 :(得分:0)

预计使用的参数在哪里:来自CommonMethod?目前尚不清楚为什么参数必须在实例化时而不是在使用时有效,因此您可以选择将其留给派生类以在使用前验证参数。

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

答案 6 :(得分:0)

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

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