Подтвердить что ты не робот

Как я могу улучшить этот дизайн?

Предположим, что наша система может выполнять действия и что для выполнения действия требуются некоторые параметры. Я определил следующий базовый класс для всех действий (упрощенных для вашего удовольствия чтения):

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. Пока все хорошо, правда? Ну нет. Анализ кода говорит мне: " не вызывает переопределяемые методы в конструкторах", что на самом деле имеет большой смысл, потому что, когда вызывается конструктор базового класса конструктор дочернего класса еще не был вызван, и поэтому метод 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 является статическим, но я не знаю... Есть ли лучший способ?

Комментарии очень приветствуются.

4b9b3361

Ответ 1

Это общая проблема проектирования в иерархии наследования - как выполнять поведение, зависящее от класса, в конструкторе. Инструменты анализа кода причины указывают на то, что проблема заключается в том, что конструктор производного класса еще не имел возможности запускаться в этой точке, а вызов виртуального метода может зависеть от состояния, которое не было инициализировано.

Итак, у вас есть несколько вариантов:

  • Игнорируйте проблему.. Если вы считаете, что разработчики должны иметь возможность написать метод проверки параметров, не полагаясь на какое-либо состояние выполнения класса, тогда задокументируйте это предположение и придерживайтесь своего дизайна.
  • Переместить логику проверки в каждый конструктор производного класса,, чтобы базовый класс выполнял только самые основные, абстрактные виды проверок, которые он должен (нулевые проверки и т.д.).
  • Дублировать логику в каждом производном классе. Этот тип дублирования кода кажется неудобным, и он открывает дверь для производных классов, чтобы забыть выполнить необходимую логику настройки или проверки.
  • Предоставить метод Initialize() определенного типа, который должен быть вызван потребителем (или factory для вашего типа), который гарантирует, что эта проверка выполняется после того, как тип полностью сконструирован, Это может быть нежелательно, так как это требует, чтобы каждый, кто создает экземпляр вашего класса, должен запомнить метод инициализации, который, по вашему мнению, может выполнить конструктор. Часто factory может помочь избежать этой проблемы - он будет единственным, кто сможет создать экземпляр вашего класса и будет вызывать логику инициализации перед возвратом этого типа потребителю.
  • Если проверка не зависит от состояния,, то фактор валидатора будет разделен на отдельный тип, который вы даже можете сделать частью подписи общего класса. Затем вы можете создать экземпляр валидатора в конструкторе, передать ему параметры. Каждый производный класс может определять вложенный класс с конструктором по умолчанию и размещать там всю логику проверки параметров. Ниже приведен пример кода этого шаблона.

По возможности, каждый конструктор выполняет проверку. Но это не всегда желательно. В этом случае я лично предпочитаю шаблон factory, потому что он поддерживает реализацию напрямую, а также обеспечивает точку перехвата, в которую позже может быть добавлено другое поведение (ведение журнала, кэширование и т.д.). Однако иногда фабрики не имеют смысла, и в этом случае я бы серьезно рассмотрел четвертый вариант создания типа валидатора в режиме ожидания.

Вот пример кода:

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

Ответ 2

Если вы откладываете на дочерний класс, чтобы все равно проверять параметры, почему бы просто не сделать это в конструкторе дочернего класса? Я понимаю принцип, к которому вы стремитесь, а именно, чтобы обеспечить, чтобы любой класс, полученный из вашего базового класса, проверял его параметры. Но даже тогда пользователи вашего базового класса могут просто реализовать версию 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; 
} 

Надеюсь, это поможет.

Ответ 3

Я бы рекомендовал применить принцип единой ответственности. Кажется, что класс Action должен нести ответственность за одно: выполнение действия. Учитывая это, проверка должна быть перенесена на отдельный объект, который отвечает только за проверку. Вы можете использовать какой-то общий интерфейс, например, для определения валидатора:

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

Затем вы можете добавить это в свой базовый конструктор и вызвать там метод проверки:

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, то вы также можете легко добавить соглашения привязки, чтобы автоматически привязывать реализации IParameterValidator соответствующим образом на основе типа TActionParameters

Ответ 4

У меня была очень похожая проблема в прошлом, и я переместил логику, чтобы проверить параметры в соответствующем классе ActionParameters. Этот подход будет работать из коробки, если классы параметров будут выровнены с классами BusinessAction.

Если это не так, это становится более болезненным. У вас есть следующие варианты (я бы предпочел первый лично):

  • Оберните все параметры в IValidatableParameters. Реализации будут выстроены бизнес-действиями и обеспечит проверку.
  • Просто подавить это предупреждение
  • Переместите эту проверку в родительские классы, но затем вы получите дублирование кода.
  • Переместите эту проверку на метод, который фактически использует параметры (но затем ваш код не работает позже)

Ответ 5

Почему бы не сделать что-то вроде этого:

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

    protected abstract TActionParameters Parameters { get; }

    protected abstract bool ParametersAreValid();

    public void CommonMethod() { ... }
}

Теперь конкретный класс должен заботиться о параметрах и обеспечивать их достоверность. Я просто принуждаю CommonMethod вызывать метод ParametersAreValid перед тем, как делать что-либо еще.

Ответ 6

Где параметры, которые предполагается использовать: изнутри CommonMethod? Непонятно, почему параметры должны быть действительными во время создания экземпляра, а не во время использования, и поэтому вы можете оставить его до производного класса для проверки параметров перед использованием.

РЕДАКТИРОВАТЬ. Учитывая то, что я знаю, проблема, похоже, является одной из специальных работ, необходимых для построения класса. Это для меня говорит о классе Factory, который используется для создания экземпляров BaseBusinessAction, где он вызывал бы виртуальный Validate() в экземпляре, который он строит, когда он его создает.

Ответ 7

Как переместить проверку в более общее место в логике. Вместо запуска проверки в конструкторе запустите его при первом (и только первом) вызове метода. Таким образом, другие разработчики могли бы построить объект, а затем изменить или исправить параметры перед выполнением действия.

Вы можете сделать это, изменив ваш getter/setter для свойства Parameters, поэтому все, что использует параметры, будет проверять их при первом использовании.