Объект значения - это шаблон проектирования, в котором объект создается для представления чего-то простого, например валюты или даты. Объект значения должен быть равен другому объекту значения, если оба объекта имеют одинаковое значение, несмотря на то, что они являются двумя разными объектами. В этой статье я расскажу, почему я считаю объекты значений полезными, и обсудю различные компромиссы при разработке объектов значений. Я буду делать примеры на C #, но эти примеры должны применяться ко многим другим основным языкам.

Покупки по выгодной цене - пример корзины для покупок:

Представьте, что вы отвечаете за разработку или поддержку кодовой базы простого интернет-магазина. Каждый товар в магазине имеет уникальный идентификатор, название и цену. В этом случае элемент может выглядеть в коде так:

Внезапно возникает новое требование: цены на товары в магазине могут указываться в нескольких валютах.

Учитывая время выхода на рынок и тот факт, что вы чувствуете, как начальник дышит вам в шею, у вас может возникнуть соблазн ввести словарь. Таким образом вы сможете узнать цену для каждой валюты. Без особых колебаний и под давлением необходимости вы выбираете простой словарь от строкового к десятичному:

Изучая изменение:

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

Не так быстро. Перенесемся на несколько месяцев, и новый разработчик подключается к проекту. Когда он видит словарь цен, он может быть озадачен тем, что именно представляет собой ключ словаря. Это обычная цена или цена со скидкой по различным кодам купонов? Это несколько валют или что-то совсем другое? Кроме того, если в словаре указаны цены в разных валютах, то он использует «доллары» или «доллары»?

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

Введите значение объекта

Здесь введение объекта-ценности может быть жизнеспособным вариантом. Резюмируем:

  1. Объект значения должен представлять что-то простое, например, валюты в этом примере.
  2. Два объекта должны быть равны, если их фактические значения равны, не обязательно, если они указывают на один и тот же объект.
  3. По возможности они должны быть неизменными.

Простая реализация такого объекта-значения может выглядеть так:

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

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

Изучение объекта валюты:

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

Operator '==' cannot be applied to operands of type 'Currency' and 'string'
Argument 1: cannot convert from 'string' to 'ValueObjects.Currency'

При осмотре сайтов звонков они могут выглядеть примерно так:

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

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

Соображения по дизайну и производительности

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

Слово предупреждения, прежде чем читать дальше. Прежде чем выполнять оптимизацию производительности, всегда проверяйте, действительно ли у вас есть проблема с производительностью, прежде чем делать это, затем обязательно измерьте, на что нужно время и где проблема.

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

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

Теперь рассмотрим случай, когда у вас есть много товаров с ценами в разных валютах. В текущей реализации это приведет к распределению многих строк «USD», «EUR» и т. Д. В этом случае вы можете выбрать, чтобы объект значения валюты выполнял интернирование строки в конструкторе валюты. Интернирование строк может быть полезно, когда у вас много строк с одним и тем же значением. В приведенном ниже примере кода каждый экземпляр объекта валюты, созданный с помощью «EUR», будет указывать на одну и ту же строку «EUR».

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

Вывод:

Объекты-значения могут быть введены в вашу кодовую базу в самых разных формах и разновидностях. Стоит ли идти на компромисс, решать вам и вашим коллегам. Я надеюсь показать вам, как объекты-значения могут повысить удобочитаемость наряду с более сильными и гибкими типами данных. Наконец, я надеюсь, что их можно аккуратно ввести в вашу кодовую базу с минимальными дополнительными затратами.