简体   繁体   English

我该如何改进这个设计?

[英]How can I improve this design?

Let's assume that our system can perform actions, and that an action requires some parameters to do its work. 假设我们的系统可以执行操作,并且操作需要一些参数来完成其工作。 I have defined the following base class for all actions (simplified for your reading pleasure): 我为所有操作定义了以下基类(简化了您的阅读乐趣):

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() { ... }
}

Only a concrete implementation of BaseBusinessAction knows how to validate that the parameters passed to it are valid, and therefore the ParametersAreValid is an abstract function. 只有BaseBusinessAction的具体实现知道如何验证传递给它的参数是否有效,因此ParametersAreValid是一个抽象函数。 However, I want the base class constructor to enforce that the parameters passed are always valid, so I've added a call to ParametersAreValid to the constructor and I throw an exception when the function returns false . 但是,我希望基类构造函数强制传递的参数始终有效,所以我在构造函数中添加了对ParametersAreValid的调用,并在函数返回false时抛出异常。 So far so good, right? 到目前为止一切都那么好吧? Well, no. 好吧,不。 Code analysis is telling me to " not call overridable methods in constructors " which actually makes a lot of sense because when the base class's constructor is called the child class's constructor has not yet been called, and therefore the ParametersAreValid method may not have access to some critical member variable that the child class's constructor would set. 代码分析告诉我“ 不要在构造函数中调用可覆盖的方法 ”这实际上很有意义,因为当调用基类的构造函数时,尚未调用子类的构造函数,因此ParametersAreValid方法可能无法访问某些子类的构造函数将设置的关键成员变量。

So the question is this: How do I improve this design? 所以问题是:我如何改进这个设计?

Do I add a Func<bool, TActionParameters> parameter to the base class constructor? 我是否将Func<bool, TActionParameters>参数添加到基类构造函数中? If I did: 如果我做了:

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;
    }

}

This would work because ValidateIt is static, but I don't know... Is there a better way? 这可行,因为ValidateIt是静态的,但我不知道......有更好的方法吗?

Comments are very welcome. 评论非常欢迎。

This is a common design challenge in an inheritance hierarchy - how to perform class-dependent behavior in the constructor. 这是继承层次结构中的常见设计挑战 - 如何在构造函数中执行依赖于类的行为。 The reason code analysis tools flag this as a problem is that the constructor of the derived class has not yet had an opportunity to run at this point, and the call to the virtual method may depend on state that has not been initialized. 代码分析工具将此标记为问题的原因是派生类的构造函数尚未有机会在此时运行,并且对虚方法的调用可能取决于尚未初始化的状态。

So you have a few choices here: 所以你有几个选择:

  1. Ignore the problem. 忽略这个问题。 If you believe that implementers should be able to write a parameter validation method without relying on any runtime state of the class, then document that assumption and stick with your design. 如果您认为实现者应该能够编写参数验证方法而不依赖于类的任何运行时状态,那么请记录该假设并坚持使用您的设计。
  2. Move validation logic into each derived class constructor, have the base class perform just the most basic, abstract kinds of validations it must (null checks, etc). 将验证逻辑移动到每个派生类构造函数中,让基类执行它必须的最基本,抽象的验证类型(空检查等)。
  3. Duplicate the logic in each derived class. 复制每个派生类中的逻辑。 This kind of code duplication seems unsettling, and it opens the door for derived classes to forget to perform the necessary setup or validation logic. 这种代码重复似乎令人不安,它为派生类忘记执行必要的设置或验证逻辑打开了大门。
  4. Provide an Initialize() method of some kind that has to be called by the consumer (or factory for your type) that will ensure that this validation is performed after the type is fully constructed. 提供某种类型的Initialize()方法,方法必须由使用者(或您的类型的工厂)调用,以确保在完全构造类型之后执行此验证。 This may not be desirable, since it requires that anyone who instantiates your class must remember to call the initialization method - which you would think a constructor could perform. 这可能是不可取的,因为它要求实例化您的类的任何人必须记住调用初始化方法 - 您认为构造函数可以执行该方法。 Often, a Factory can help avoid this problem - it would be the only one allowed to instantiate your class, and would call the initialization logic before returning the type to the consumer. 通常,工厂可以帮助避免这个问题 - 它将是唯一一个允许实例化您的类,并在将类型返回给使用者之前调用初始化逻辑。
  5. If validation does not depend on state, then factor the validator into a separate type, which you could even make part of the generic class signature. 如果验证不依赖于状态,则将验证器分解为单独的类型,您甚至可以将其作为通用类签名的一部分。 You could then instantiate the validator in the constructor, pass the parameters to it. 然后,您可以在构造函数中实例化验证器,将参数传递给它。 Each derived class could define a nested class with a default constructor, and place all parameter validation logic there. 每个派生类都可以使用默认构造函数定义嵌套类,并将所有参数验证逻辑放在那里。 A code example of this pattern is provided below. 下面提供了该模式的代码示例。

When possible, have each constructor perform the validation. 如果可能,让每个构造函数执行验证。 But this isn't always desirable. 但这并不总是令人满意的。 In that case, I personally, prefer the factory pattern because it keeps the implementation straight forward, and it also provides an interception point where other behavior can be added later (logging, caching, etc). 在这种情况下,我个人更喜欢工厂模式,因为它使实现保持直接,并且它还提供了一个拦截点,可以在以后添加其他行为(日志记录,缓存等)。 However, sometimes factories don't make sense, and in that case I would seriously consider the fourth option of creating a stand-along validator type. 但是,有时工厂没有意义,在这种情况下,我会认真考虑创建一个独立验证器类型的第四个选项。

Here's the code example: 这是代码示例:

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

If you are deferring to the child class to validate the parameters anyway, why not simply do this in the child class constructor? 如果您仍然推迟使用子类来验证参数,为什么不在子类构造函数中简单地执行此操作? I understand the principle you are striving for, namely, to enforce that any class that derives from your base class validates its parameters. 我理解您正在努力的原则,即强制执行从您的基类派生的任何类验证其参数。 But even then, users of your base class could simply implement a version of ParametersAreValid() that simply returns true, in which case, the class has abided by the letter of the contract, but not the spirit. 但即使这样,基类的用户也可以简单地实现一个只返回true的ParametersAreValid()版本,在这种情况下,类已经遵守了合同的字母,而不是精神。

For me, I usually put this kind of validation at the beginning of whatever method is being called. 对我来说,我通常会在调用任何方法的开头进行这种验证。 For example, 例如,

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; 
} 

I hope this helps. 我希望这有帮助。

I would recommend applying the Single Responsibility Principle to the problem. 我建议将单一责任原则应用于问题。 It seems that the Action class should be responsible for one thing; 似乎Action类应该对一件事负责; executing the action. 执行动作。 Given that, the validation should be moved to a separate object which is responsible only for validation. 鉴于此,应将验证移至单独的对象,该对象仅负责验证。 You could possibly use some generic interface such as this to define the validator: 您可以使用一些通用接口来定义验证器:

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

You can then add this to your base constructor, and call the validate method there: 然后,您可以将其添加到基础构造函数中,并在那里调用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");
}

There is a nice hidden benefit to this approach, which is it allows you to more easily re-use validation rules that are common across actions. 这种方法有一个很好的隐藏好处,它允许您更轻松地重用跨操作通用的验证规则。 If your using an IoC container, then you can also easily add binding conventions to automatically bind IParameterValidator implementations appropriately based on the type of TActionParameters 如果您使用IoC容器,那么您还可以轻松添加绑定约定,以根据TActionParameters的类型自动绑定IParameterValidator实现

I had a very similar issue in the past and I ended up moving the logic to validate parameters to the appropriate ActionParameters class. 我过去遇到了一个非常类似的问题,最后我将逻辑移动到相应的ActionParameters类来验证参数。 This approach would work out of the box if your parameter classes are lined up with BusinessAction classes. 如果您的参数类与BusinessAction类对齐,则此方法可以立即使用。

If this is not the case, it gets more painful. 如果情况并非如此,则会更加痛苦。 You have the following options (I would prefer the first one personally): 你有以下选择(我更喜欢第一个个人):

  1. Wrap all the parameters in IValidatableParameters . IValidatableParameters包装所有参数。 The implementations will be lined up with business actions and will provide validation 这些实现将与业务操作排成一行,并将提供验证
  2. Just suppress this warning 只是压制这个警告
  3. Move this check to parent classes, but then you end up with code duplication 将此检查移至父类,但最终会导致代码重复
  4. Move this check to the method that actually uses the parameters (but then your code fails later) 将此检查移动到实际使用参数的方法(但稍后您的代码会失败)

Why not do something like this: 为什么不这样做:

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

    protected abstract TActionParameters Parameters { get; }

    protected abstract bool ParametersAreValid();

    public void CommonMethod() { ... }
}

Now the concrete class has to worry about the parameters and ensuring their validity. 现在,具体类必须担心参数并确保其有效性。 I would just enforce the CommonMethod calling the ParametersAreValid method prior to doing anything else. 我会在执行任何其他操作之前强制执行CommonMethod调用ParametersAreValid方法。

Where are the parameters anticipated to be used: from within CommonMethod? 预期使用的参数在哪里:来自CommonMethod? It is not clear why the parameters must be valid at the time of instantiation instead of at the time of use and thus you might choose to leave it up to the derived class to validate the parameters before use. 目前尚不清楚为什么参数必须在实例化时而不是在使用时有效,因此您可以选择将其保留到派生类以在使用前验证参数。

EDIT - Given what I know the problem seems to be one of special work needed on construction of the class. 编辑 - 鉴于我所知道的问题似乎是建设班级所需的特殊工作之一。 That, to me, speaks of a Factory class used to build instances of BaseBusinessAction wherein it would call the virtual Validate() on the instance it builds when it builds it. 对我来说,这是一个用于构建BaseBusinessAction实例的Factory类,其中它将在构建它时构建的实例上调用虚拟Validate()。

How about moving the validation to a more common location in the logic. 如何将验证移动到逻辑中更常见的位置。 Instead of running the validation in the constructor, run it on the first (and only the first) call to the method. 而不是在构造函数中运行验证,而是在对方法的第一次(也是唯一一次)调用上运行它。 That way, other developers could construct the object, then change or fix the parameters before executing the action. 这样,其他开发人员可以构造对象,然后在执行操作之前更改或修复参数。

You could do this by altering your getter/setter for the Parameters property, so anything that uses the paramters would validate them on the first use. 你可以通过改变Parameters属性的getter / setter来做到这一点,所以使用参数的任何东西都会在第一次使用时验证它们。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM