Pull to refresh

«Запах» кода: автоматические свойства

Reading time 4 min
Views 18K
Original author: Mark Seemann
Это третий пост из серии о Poka-yoke проектировании – также известном, как инкапсуляция.

Автоматические свойства – одна из наиболее излишних возможностей в C#. Я знаю, что многие люди очень их любят, но они решают проблему, с которой вы и сталкиваться не должны.

Я абсолютно согласен с тем, что код, который выглядит следующим образом, абсолютно избыточен:
private string name;
public string Name
{
    get { return this.name; }
    set { this.name = value; }
}

Однако, решением проблемы не является перепись это кода следующим образом:
public string Name { get; set; }

Проблема с кодом в первом примере заключается не в его церемониальности. Проблема заключается в том, что он нарушает инкапсуляцию. На самом деле
“[…] геттеры и сеттеры не обеспечивают инкапсуляцию или сокрытие информации: они являются способом нарушения этих принципов, который легализован языком программирования.”
James O. Coplien & Gertrud Bjørnvig. Lean Architecture. Wiley. 2010. p. 134.

В то время как лично я считаю, что автоматические свойства имеют своё применение, я очень редко нахожу им его. Они никогда не подходят для ссылочных типов и только изредка применимы в типах-значениях

«Запах» кода: автоматическое свойство ссылочного типа
Прежде всего, давайте рассмотрим очень большой набор свойств, которые раскрываются ссылочным типом.
В случае ссылочных типов, возможным значением является null. Однако, с учётом Poka-yoke дизайна, null никогда не является подходящим значением, поскольку он ведёт к NullReferenceExceptions. Null Object паттерн является лучшей альтернативой, позволяющей справлять с ситуациями, когда значение может быть неопределённым.
Другими словами, автоматические свойства вроде свойства Name никогда не являются подходящими. Сеттер обязан иметь какое-либо защитное выражение для того, чтобы защититься от значения null (и, возможно, от других не допустимых значений). Вот основательный пример:
private string name;
public string Name
{
    get { return this.name; }
    set 
    {
        if (value == null)
        {
            throw new ArgumentNullException("value");
        }
        this.name = value; 
    }
}

В качестве альтернативы, защитное выражение может также осуществлять проверку на null и обеспечивать значение по умолчанию:
private string name;
public string Name
{
    get { return this.name; }
    set 
    {
        if (value == null)
        {
            this.name = "";
            return;
        }
        this.name = value; 
    }
}

Однако эта реализация содержит нарушение POLA, потому что геттер иногда возвращает значения, отличные от присвоенных. Можно исправить эту проблему, посредством добавления ассоциированного поля типа Boolean, которое показывает было ли имени присвоено null, чтобы null можно было бы возвратить из сеттера в этом исключительном случае, но это ведёт к другому «запаху» в коде.

«Запах» кода: автоматические свойства типов-значений
Если тип свойства является типом значения, случай становится менее ясным, поскольку значения не могут принимать null. Это означает, что сторожок на null никогда не подходит. Однако, прямолинейное потребление типа-значения также может быть не подходящим. По сути, допустимо только то, чтобы класс осмысленно мог принять и обработать любое значения этого типа.
Если, например, класс может реально работать только с определённым подмножеством всех возможных значений, может быть введено защитное выражение. Рассмотрим пример:
public int RetryCount { get; set; }

Это свойства могло бы быть использовано для установки нужного количества попыток для выполнения заданной операции. Проблема заключается в том, что автоматическое свойство позволяет установить отрицательное значение, а это бессмысленно. Одним возможным решением является добавление защитного выражения:
private int retryCount;
public int RetryCount
{
    get { return this.retryCount; }
    set
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException();
        }
        this.retryCount = value;
    }
}

Однако, во многих случаях, раскрытие свойства примитивного типа, скорее всего является случаем «одержимости примитивами».

Исправленный дизайн: защитное выражение
Как я и описал ранее, самым быстрым способом исправить проблему с автоматическим свойством – реализовать свойство с защитным выражением. Это даёт гарантию, что инварианты класса правильно инкапсулированы.

Исправленный дизайн: свойство типа-значения
Когда автоматическое свойство является типом-значения, реализация защитного выражения всё ещё может иметь смысл. Однако, когда свойство является реальным симптомом «одержимости примитивами», лучшей альтернативой является введение правильного объекта-значения.
Рассмотрим, в качестве примера, следующее свойство:
public int Temperature { get; set; }

Это плохой дизайн по нескольким причинам. Он не связан со смыслом единицы измерения и позволяет присвоение неограниченных значений. Что произойдёт, если будет присвоено -100? Если единицей измерения является Цельсий, то всё нормально, но если – Кельвин, то налицо ошибка. В независимости от единицы измерения, попытка присвоения int.MinValue должна провалиться.
Более устойчивого дизайна можно было бы достигнуть путём введения нового типа Temperature и, изменив тип свойства на введённый. Кроме защиты инвариантов, новый класс также мог бы инкапсулировать конвертацию между различными мерами температур.
Однако, если объект-значение реализован как ссылочный тип, ситуация становится эквивалентной ситуации, описанной выше и проверка на null является необходимой. Использование автоматических свойств уместно, если объект-значение реализован как тип-значение.
Вывод: автоматические свойства редко являются уместными. На самом деле, они являются допустимыми только тогда, когда тип свойства является типом-значением и все вероятные значения являются допустимыми. Поскольку существует несколько случаев, когда автоматические свойства являются уместными, их использование нельзя полностью исключать, но нужда в их использовании должна рассматриваться как причина для дополнительного расследования. Использование автоматических свойств является «запахом» в коде, но не анти-паттерном.
Стоит заметить, что свойства также могут нарушать закон Деметры, но это уже тема для будущего поста.
Tags:
Hubs:
+3
Comments 2
Comments Comments 2

Articles