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

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

public abstract class BaseBusinessAction 
    : 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 метод не может иметь доступа к некоторой критической членской переменной, которую установил бы конструктор дочернего класса.

Таким образом, вопрос - это: Как я улучшаю этот дизайн?

Сделайте я добавляю a Func параметр конструктору базового класса? Если я сделал:

public class MyAction
{
    public MyAction(MyParameters actionParameters, bool something) : base(actionParameters, ValidateIt)
    {
        this.something = something;
    }

    private bool something;

    public static bool ValidateIt()
    {
       return something;
    }

}

Это работало бы потому что ValidateIt статично, но я не знаю... Существует ли лучший путь?

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

16
задан Klaus Byskov Pedersen 15 March 2010 в 16:56
поделиться

7 ответов

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

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

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

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

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

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; /*...*/ }
    }
}
7
ответ дан 30 November 2019 в 22:43
поделиться

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

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

  1. Обернуть все параметры в IValidatableParameters . Реализации будут согласованы с бизнес-действиями и обеспечат валидацию
  2. Просто подавите это предупреждение
  3. Перенесите эту проверку в родительские классы, но тогда вы получите дублирование кода
  4. Перенесите эту проверку в метод, который фактически использует параметры (но потом ваш код не работает)
1
ответ дан 30 November 2019 в 22:43
поделиться

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

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

0
ответ дан 30 November 2019 в 22:43
поделиться

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

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

    protected abstract TActionParameters Parameters { get; }

    protected abstract bool ParametersAreValid();

    public void CommonMethod() { ... }
}

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

0
ответ дан 30 November 2019 в 22:43
поделиться

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

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

0
ответ дан 30 November 2019 в 22:43
поделиться

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

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

5
ответ дан 30 November 2019 в 22:43
поделиться

Я бы рекомендовал применить к этой проблеме Принцип единственной ответственности . Кажется, что класс 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

3
ответ дан 30 November 2019 в 22:43
поделиться
Другие вопросы по тегам:

Похожие вопросы: