Это пятый пост из серии о Poka-yoke проектировании – также известном, как инкапсуляция.
Конструкторы по умолчанию являются «запахом» в коде. Именно так. Это может звучать возмутительно, но примем во внимание следующее: объектное ориентирование это инкапсуляция поведения и данных в связные куски кода (классы). Инкапсуляция означает, что класс должен защищать целостность данных, которые он инкапсулирует. Когда данные являются необходимыми, они должны быть затребованы через конструктор. Напротив, конструктор по умолчанию говорит о том, что никаких внешних данных не требуется. Это довольно слабое утверждение, касательно инвариантов класса.
Пожалуйста, примите во внимание, что этот пост описывает «запах». Это означает, что когда определённая идиома или паттерн (в этом случае – конструктор по умолчанию) обнаружены в коде – это должно вызвать дополнительное исследование.
Как мы далее отметим, есть несколько сценариев, когда с конструкторами по умолчанию всё нормально, таким образом, целью этого поста является не разгром конструкторов по умолчанию. Целью является предоставление пищи для размышлений.
Если вы читали мою книгу, то вы уже знаете, что инъекция в конструктор является доминирующим DI-паттерном именно потому, что это статически показывает зависимости и защищает целостность класса, гарантируя, что инициализированный потребитель этих зависимостей всегда находится в непротиворечивом состоянии. Это отказоустойчивый дизайн, потому что компилятор усиливает эти взаимоотношения, обеспечивая быструю обратную связь.
Этот принцип простирается далеко за DI. В предыдущем посте я описал то, как конструктор с аргументами статически раскрывает необходимые аргументы.
public class Fragrance : IFragrance
{
private readonly string name;
public Fragrance(string name)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
this.name = name;
}
public string Spread()
{
return this.name;
}
}
Класс Fragrance защищает целостность имени, требуя его через конструктор. Так как классу требуется имя для реализации собственного поведения, то он запрашивать его через конструктор является хорошей практикой. Конструктор по умолчанию не был бы отказоустойчивым, поскольку он вносил бы временную связанность.
Примите во внимание, что объекты должны быть контейнерами повеления и данных. В то время как объекты содержат данные, они должны быть инкапсулированы. В случае (очень распространённом), когда невозможно определить имеющих смысл значений по умолчанию, данные должны быть переданы объекту через конструктор. Таким образом, наличие конструкторов по умолчанию может индицировать нарушение инкапсуляции.
В каких случаях использование конструкторов по умолчанию оправдано?
Есть сценарии, в которых использование конструкторов по умолчанию вполне оправданно (уверен, что таких сценариев больше, чем приведено ниже):
- Если конструктор может присвоить осмысленные значения по умолчанию всем содержимым полям, он по-прежнему защищает свои инварианты. В качестве примера, конструктор по умолчанию класса UriBuilder инициализирует свои внутренние значения в непротиворечивый набор, который установит Uri в значение localhost, до тех пор пока одно или несколько его свойств будут последовательно модифицированы. Вы можете соглашаться или нет с поведением по умолчанию, но оно непротиворечиво и обеспечивает инкапсуляцию.
- Если класс не содержит данные, очевидно, что и защищать нечего. Однако, это может быть симптомом «запаха» «зависть к чужим членам», который обычно можно считать доказанным, если класс, о котором идёт речь является конкретным.
Если такой класс может быть легко объявлен статическим, то это отчётливый признак указанного «запаха».
Если, с другой стороны, класс реализует интерфейс, это может быть признаком того, что он представляет чистое поведение.
Класс, представляющий чистое поведение, посредством реализации интерфейса – вещь не обязательно плохая. Такая конструкция может оказаться очень мощной.
В заключение, наличие конструктора по умолчанию должно быть сигналом для того, чтобы остановиться и подумать об инвариантах рассматриваемого класса. Гарантирует ли целостность инкапсулируемых данных конструктор по умолчанию? Если да, то конструктор по умолчанию подходит, если нет, то не подходит. На моём опыте, конструкторы по умолчанию – скорее исключение, чем правило.
Автор: EngineerSpock