我该如何改进这种设计?
假设我们的系统可以执行操作,并且操作需要一些参数才能完成其工作。 我已经定义了所有动作以下基本类(简化您的阅读快感):我该如何改进这种设计?
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
是静态的,但我不知道...有没有更好的办法?
评论是非常受欢迎的。
这是继承层次结构中常见的设计挑战 - 如何在构造函数中执行与类相关的行为。代码分析工具将此标记为问题的原因是派生类的构造函数尚未有机会在此时运行,并且对虚拟方法的调用可能取决于尚未初始化的状态。
所以,你有几个选择这里:
- 忽视的问题。如果您认为实现者应该能够在不依赖任何类的运行时状态的情况下编写参数验证方法,请记录该假设并坚持使用您的设计。
- 将验证逻辑移入每个派生类构造函数,让基类执行最基本的抽象类型验证(空检查等)。
- 复制每个派生类中的逻辑。这种代码复制似乎令人不安,它为派生类打开大门忘记来执行必要的设置或验证逻辑。
- 提供某种类型的初始化()方法必须由消费者(或您的类型的工厂)调用,以确保在完全构造类型后执行此验证。这可能不是所希望的,因为它要求任何实例化你的类的人都必须记得调用初始化方法 - 你会认为构造函数可以执行。通常情况下,Factory可以帮助避免这个问题 - 它是唯一允许实例化类的人,并且在将类型返回给使用者之前调用初始化逻辑。
- 如果验证不依赖于状态,则将验证器归入单独的类型,您甚至可以将其作为通用类签名的一部分。然后你可以在构造函数中实例化验证器,并将参数传递给它。每个派生类可以使用默认构造函数定义嵌套类,并将所有参数验证逻辑放置在那里。下面提供了这种模式的代码示例。
如果可能,让每个构造函数执行验证。但这并不总是可取的。在这种情况下,我个人更喜欢工厂模式,因为它保持了实现的直观性,并且它还提供了一个拦截点,其他行为可以稍后添加(日志记录,缓存等)。但是,有时工厂没有意义,在那种情况下,我会认真考虑创建一个stand-along验证器类型的第四个选项。
下面的代码示例:
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方法的观点。我已经有一个工厂类可以做到这一点。 – 2010-03-15 17:17:01
是的,我认为我更喜欢这个,因为它使得验证更加明确,而不仅仅依赖于子类来验证参数,就像其他海报提出的那样。 – 2010-03-15 17:23:45
为什么不能做这样的事情:
public abstract class BaseBusinessAction<TActionParameters>
: where TActionParameters : IActionParameters
{
protected abstract TActionParameters Parameters { get; }
protected abstract bool ParametersAreValid();
public void CommonMethod() { ... }
}
现在具体的类有担心的参数,并确保其有效性。在执行其他任何操作之前,我只需要执行CommonMethod
即可调用ParametersAreValid
方法。
我在过去有一个非常类似的问题,我最终将逻辑移动到适当的ActionParameters
类验证参数。如果您的参数类与BusinessAction类排列在一起,此方法即可使用。
如果情况并非如此,则会变得更加痛苦。您有以下选项(我个人比较喜欢第一个):
- 将所有参数包装在
IValidatableParameters
中。这些实现将排队与业务操作,并提供验证 - 就禁止这种警告
- 移动这个检查父类,但你最终与代码重复
- 推动这一检查,以实际使用的方法参数(但你的代码稍后会失败)
如果你推迟到子类验证参数,为什么不简单地在子类构造函数中做这个?我理解你正在努力的原则,即强制从基类派生的任何类验证其参数。但即使如此,你的基类的用户可以简单地实施ParametersAreValid()
版本,只是简单地返回true,在这种情况下,班级遵守合同的信件,但不遵守精神。
对我来说,我通常会在被调用的任何方法开始时进行这种验证。例如,
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这正是我的想法。儿童班甚至可能不需要任何特别的确认,在这种情况下,虚拟电话毫无意义。只有派生类的设计者知道它的需求是什么。这很好地避免了在构造函数中调用虚拟方法。 – 2010-03-15 17:14:17
是的,这是一个好点。 – 2010-03-15 17:20:28
如果预计要使用的参数:从内部的commonMethod?目前尚不清楚为什么参数必须在实例化时有效,而不是在使用时才有效,因此您可以选择将其留给派生类以在使用前验证参数。
编辑 - 根据我所知,这个问题似乎是建造班级所需的特殊工作之一。对我来说,讲的是一个用于构建BaseBusinessAction实例的Factory类,其中它会在构建它时构建的实例上调用虚拟Validate()。
'CommonMethod'就是为了说明的目的。实际上,有很多常见的方法都取决于参数的有效性。 – 2010-03-15 17:19:29
我会建议应用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容器,那么您还可以轻松添加绑定约定,以根据TAction类型自动绑定IParameterValidator实现。参考
+1谢谢你的建议。我正在使用IoC容器,并且您对SRP是正确的。 – 2010-03-15 17:25:57
这个解决了编译问题,并保持了最初设计的精神/目的。它最终增加了进一步的灵活性水平。 – Seb 2010-03-15 17:31:28
如何将验证移动到逻辑中更常见的位置。不是在构造函数中运行验证,而是在第一次(也是第一次)调用方法时运行它。这样,其他开发人员就可以构建该对象,然后在执行该操作之前更改或修复参数。
你可以通过改变你的参数属性的getter/setter来做到这一点,所以任何使用参数的东西都会在首次使用时验证它们。
+1,你让我为了实验而编写代码。 – Roman 2010-03-15 17:05:00
ValidateIt方法是否需要访问MyParameters私有才能验证它?也许你应该将MyParameters实例传递给ValidateIt。 或者,MyParameters可以验证自己吗? 或者,也许您计划通过Action基类实现的功能可以通过拥有对操作的引用来更好地实现,而不是超级引用它们? – Tormod 2010-03-15 17:15:38