假设我们的系统可以执行操作,并且操作需要一些参数来完成其工作。 我为所有操作定义了以下基类(为了您的阅读乐趣而简化):
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
是静态的,但我不知道......有更好的方法吗?
非常欢迎评论。
答案 0 :(得分:7)
这是继承层次结构中的常见设计挑战 - 如何在构造函数中执行依赖于类的行为。代码分析工具将此标记为问题的原因是派生类的构造函数尚未有机会在此时运行,并且对虚方法的调用可能取决于尚未初始化的状态。
所以你在这里有一些选择:
如果可能,让每个构造函数执行验证。但这并不总是令人满意的。在这种情况下,我个人更喜欢工厂模式,因为它使实现保持直接,并且它还提供了一个拦截点,可以在以后添加其他行为(日志记录,缓存等)。但是,有时工厂没有意义,在这种情况下,我会认真考虑创建一个独立验证器类型的第四个选项。
以下是代码示例:
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类对齐,则此方法可以立即使用。
如果情况并非如此,则会更加痛苦。你有以下选择(我更喜欢第一个个人):
IValidatableParameters
中的所有参数。这些实现将与业务操作排成一行,并将提供验证答案 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来做到这一点,所以使用参数的任何东西都会在第一次使用时验证它们。