Объектов достаточно

Я не говорю о глобальном изменяемом состоянии, это слишком просто. Я имею в виду любые глобально доступные статические данные, которые неизменны.

Некоторое время назад я прочитал книгу по Smalltalk, и она заставила меня взглянуть на концепцию данных под другим углом. Будучи чистым языком ООП, (почти) все в Smalltalk является объектом. Конечно, вы можете создать общедоступную переменную класса, но это просто не так, как там делают.

Есть несколько способов представления в вашей программе некоторого многократно используемого фрагмента данных. Наиболее популярные из них представляют общедоступную константу и создают файл конфигурации. Color.RED, ContentTypeHeader.JSON, Math.PI, translation.fr.yaml - все они кажутся хорошими и прекрасными. Но так ли они?

Что не так со статическими данными

Во-первых, их нельзя украсить
нельзя украсить статические данные. Таким образом, у вас будет меньше занятий. А если так, то вероятность того, что однажды размер вашего класса достигнет потолка, больше. Как ни странно, многие люди не решаются создать небольшой класс. Хотя маленькие классы - это и есть SOLID.
Классический пример - число Пи. Это реальное число. Так что мой код, вероятно, будет выглядеть следующим образом:

interface RealNumber
{
    public function value(): string;
}

class Pi implements RealNumber
{
    public function value(): string
    {
        return '3.14159265359';
    }
}

Итак, что бы вы сделали, если бы вам нужно было вывести Пи только с двумя десятичными знаками? Вот мой подход:

class WithDecimalPlaces implements RealNumber
{
    private $decorated;
    private $decimalPlaces;

    public function __construct(RealNumber $decorated, int $decimalPlaces)
    {
        $this->decorated = $decorated;
        $this->decimalPlaces = $decimalPlaces;
    }

    public function value(): string
    {
        return
            (string) round(
                (float) $this->decimalPlaces->value(),
                $this->decimalPlaces
            );
    }
}

Его можно украсить, поэтому моя функциональность сосредоточена в небольших классах. Наверное, это хороший пример того, о чем говорил Роберт Мартин в своем Принципе единой ответственности.

Это несовместимо
. Шаблон декоратора является примером, вероятно, наиболее фундаментальной концепции ООП - композиции, которая также нарушается концепцией данных.

Статические общедоступные данные, такие как 3.1416 или hello, не являются абстракцией, поэтому у них не может быть значимого интерфейса, поэтому их нельзя внедрить в другой класс. Таким образом, более вероятно, что функциональность, работающая с этими данными, будет распространена по всей кодовой базе, увеличивая дублирование.

Позвольте мне подробнее остановиться на этом моменте. Каков ваш подход, когда вам нужно выполнить некоторую арифметику, например округлить числа, вычислить площадь или перевести деньги из второстепенных в основные единицы? Не лгите себе: часто вы слишком ленивы, чтобы выделить эту функциональность в класс. Но вы знаете, что вам следовало это сделать. Вы когда-нибудь задумывались, почему это так? У меня есть, и я думаю, что это тот случай, когда вам не хватает полезной абстракции. Создать класс намного проще, если у вас есть интерфейс, который уже соответствует вашим целям, например RealNumber из предыдущего примера.

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

class CircleArea implements RealNumber
{
    private $radius;

    public function __construct(RealNumber $radius)
    {
        $this->radius = $radius;
    }

    public function value(): string
    {
        return
            (new Product(
                new Pi(),
                new Square($this->radius)
            ))
                ->value()
            ;
    }
}

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

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

Он тесно связан
Когда вы вызываете константу некоторого класса, вы тесно связаны с этим классом. Это так просто. Так что, спросите вы, в этом нет ничего страшного. Что ж, если вы никогда не реорганизуете свой код, если ваши классы никогда не меняются, если их семантика никогда не меняется, тогда да, это не имеет большого значения. В противном случае это так.

Заключительный пример

Это для тех, кто все еще думает, что не может жить без чистых данных. Очень типичный подход для средства перевода - поместить переведенный текст в какой-нибудь файл yaml / json / .properties. Я тоже решаю это с помощью предметов. Скажем, у меня есть понятие о статусе заказа, которое может сообщить мне его название. Этот факт отражен в моем коде в виде интерфейса:

interface OrderStatus
{
    public function name(): string;
}

Если язык по умолчанию - английский, то какой-то конкретный класс будет выглядеть следующим образом:

class Initiated implements OrderStatus
{
    public function name(): string
    {
        return 'Initiated';
    }
}

Когда мне нужен перевод статуса на другой язык, это требование превращается в новый объект:

class TranslatedTo implements OrderStatus
{
    private $original;
    private $language;

    public function __construct(OrderStatus $original, Language $language)
    {
        $this->original = $original;
        $this->language = $language;
    }

    public function name(): string
    {
        return $this->translation()[$this->language->id()][$this->original->name()];
    }

    private function translation()
    {
        return [
            (new Russian())->id() => [
                (new Initiated())->name() => 'Инициализирован'
            ]
        ];
    }
}

Домашнее задание

Как можно избавиться от файла конфигурации? С какими проблемами вы столкнетесь? В чем основная причина этих проблем? Например, вы используете разные значения в зависимости от среды, в которой работает ваш код. Можете ли вы разделить корень композиции в зависимости от среды, чтобы вы могли инициализировать правильные объекты? Или у вас вообще есть корень композиции? Или частая проблема с перекомпиляцией? Или частое развертывание есть? Разве подход с использованием файла конфигурации не является лишь замалчиванием проблемы?

В заключение

Я не говорю об использовании объектов ради ООП или ради использования объектов. У этого подхода есть очень конкретные преимущества. Так что я надеюсь, что в следующий раз, когда вы объявите константу, это будет осознанный выбор, а не традиция из нашего процедурного прошлого.

А вот ветка Hacker News, откуда я украл свой заголовок.