День 1524. #ЗаметкиНаПолях
Храните Информацию в Её Высшей Форме. Начало
Может быть несколько представлений некоторой части информации; вы не должны ограничивать себя только одним из них. Вместо этого храните источник этой информации.
Допустим, мы создаём онлайн-кинотеатр и нам нужно хранить продолжительность фильмов в базе. Во внешнем интерфейсе она представлена как «1ч 47мин». Но в каком виде её хранить? В виде строки «1ч 47мин», но что, если мы решим изменить формат на 1:47 или «107 минут»? Поэтому нужно хранить её в форме, которую можно легко преобразовать в любой формат, то есть в виде целого количества минут. Это высшая форма информации о продолжительности фильма.
Этот совет можно перефразировать как: «Храните исходник, а не исполнение.»
Звучит тривиально. Вот более сложный пример. Есть сущность Customer и объект-значение LoyaltyPoints:
public class Customer : Entity
{
public LoyaltyPoints Points { get; private set; }
public void AddPoints(LoyaltyPoints pts)
=> Points += pts;
public void RedeemPoints(LoyaltyPoints pts)
{
if (Points < 250 || pts > Points)
throw new Exception();
Points -= pts;
}
}
У нас два варианта использования:
1) Когда клиент размещает заказ, объект Order вычисляет баллы лояльности на основе суммы заказа и вызывает AddPoints для клиента.
2) Клиент может использовать баллы лояльности, когда их минимальное значение составляет 250.
Приведённый выше код идеально отвечает этим требованиям.
Допустим, теперь клиент может обновить существующий заказ. Когда товар удаляется из заказа, объект заказа должен рассчитать разницу в баллах лояльности и вычесть её из суммы клиента. Вот три возможных решения:
1) Повторно использовать RedeemPoints для вычитания. Но этот метод проверяет минимальное значение 250 и выдаст исключение для клиента без баллов лояльности.
2) Использовать AddPoints, передав отрицательное число. Но по бизнес-правилам LoyaltyPoints не может быть отрицательным.
3) Ввести отдельный метод, который не проверяет минимум в 250:
public void SubtractPoints(LoyaltyPoints pts)
=> Points -= points;
Но теперь у нас два публичных метода, которые выполняют вычитание, и неочевидно, какой когда использовать. Это является признаком того, что мы раскрываем детали реализации.
Все 3 решения — лишь попытка разобраться с последствиями неправильного дизайна. Нужно хранить исходные данные расчёта, а не его результат. Остаток баллов лояльности - производное от двух частей информации: сколько баллов клиент заработал и сколько он использовал.
Вместо сохранения остатка, нужно хранить два исходных значения и вычислять остаток на лету:
public class Customer : Entity
{
public LoyaltyPoints Earned { get; private set; }
public LoyaltyPoints Redeemed { get; private set; }
public LoyaltyPoints Points
=> Earned - Redeemed;
public void IncreasePoints(LoyaltyPoints pts)
=> Earned += pts;
public void ReducePoints(LoyaltyPoints pts)
=> Earned -= pts;
…
}
Одно поле не передавало должным образом значение метода SubtractPoints. Это может означать любой из двух вариантов использования:
- снятие заработанных баллов,
- добавление использованных баллов.
Между тем, различие между двумя вариантами использования имеет решающее значение, поскольку один из них должен содержать валидацию (использование баллов), а другой — нет (корректировка заработанных баллов). Разделив поле на две части, мы устраняем эту двусмысленность.
Хотя примеры выше различаются, принцип один. Мы вычисляем строку продолжительности фильма на лету из её источника, и также вычисляем оставшиеся баллы из заработанных и использованных. В обоих сценариях мы не замыкаемся на определённом формате. Например, в дополнение к показу покупателю оставшихся баллов система может также отображать общее количество использованных баллов, чтобы показать, сколько он сэкономил.
Окончание следует…
Источник: https://enterprisecraftsmanship.com/posts/storing-information-in-its-highest-form/