Давайте предположим, что наша система может выполнить действия, и что действие требует, чтобы некоторые параметры сделали его работу. Я определил следующий базовый класс для всех действий (упрощенный для Вашего удовольствия чтения):
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
статично, но я не знаю... Существует ли лучший путь?
Комментарии очень приветствуются.
Это общая проблема проектирования в иерархии наследования - как выполнять класс-зависимое поведение в конструкторе. Инструменты анализа кода причины отмечают, что проблема заключается в том, что конструктор производного класса еще не имел возможности работать в этой точке, и вызов виртуального метода может зависеть от состояния, которое не было инициализировано.
Итак, у вас есть несколько вариантов:
По возможности пусть каждый конструктор выполняет проверку. Но это не всегда желательно. В этом случае я лично предпочитаю фабричный шаблон, потому что он обеспечивает прямую реализацию, а также обеспечивает точку перехвата, где позже можно добавить другое поведение (ведение журнала, кеширование и т. Д.). Однако иногда фабрики не имеют смысла, и в этом случае я бы серьезно рассмотрел четвертый вариант создания резервного типа валидатора.
Вот пример кода:
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; /*...*/ }
}
}
У меня была очень похожая проблема в прошлом, и в итоге я переместил логику для проверки параметров в соответствующий класс ActionParameters
. Этот подход будет работать из коробки, если ваши классы параметров выровнены с классами BusinessAction.
Если это не так, это становится более болезненным. У вас есть следующие варианты (я бы предпочел первый лично):
IValidatableParameters
. Реализации будут согласованы с бизнес-действиями и обеспечат валидацию Где предполагается использовать параметры: из CommonMethod? Непонятно, почему параметры должны быть действительными во время создания экземпляра, а не во время использования, и поэтому вы можете оставить это производному классу для проверки параметров перед использованием.
РЕДАКТИРОВАТЬ - Учитывая то, что я знаю, проблема, похоже, связана с особой работой, необходимой для создания класса. Для меня это говорит о классе Factory, используемом для создания экземпляров BaseBusinessAction, в котором он будет вызывать виртуальный Validate () в экземпляре, который он создает, когда он его создает.
Почему бы не сделать что-то вроде этого:
public abstract class BaseBusinessAction<TActionParameters>
: where TActionParameters : IActionParameters
{
protected abstract TActionParameters Parameters { get; }
protected abstract bool ParametersAreValid();
public void CommonMethod() { ... }
}
Теперь конкретный класс должен позаботиться о параметрах и обеспечении их достоверности. Я бы просто принудил CommonMethod
вызвать метод ParametersAreValid
, прежде чем делать что-либо еще.
Как насчет переноса валидации в более распространенное место в логике. Вместо того чтобы запускать проверку в конструкторе, запускайте ее при первом (и только первом) вызове метода. Таким образом, другие разработчики смогут создавать объект, затем изменять или исправлять параметры перед выполнением действия.
Это можно сделать, изменив getter/setter для свойства Parameters, чтобы все, что использует параметры, проверяло их при первом использовании.
Если вы все равно откладываете проверку параметров на дочерний класс, почему бы просто не сделать это в конструкторе дочернего класса? Я понимаю принцип, к которому вы стремитесь, а именно: обеспечить, чтобы любой класс, производный от вашего базового класса, проверял свои параметры. Но даже в этом случае пользователи вашего базового класса могут просто реализовать версию 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;
}
Надеюсь, это поможет.
Я бы рекомендовал применить к этой проблеме Принцип единственной ответственности . Кажется, что класс 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