Когда примитивная одержимость не является запахом кода?

22

Недавно я прочитал множество статей, в которых примитивная одержимость описывается как запах кода.

Есть два преимущества избегания примитивной одержимости:

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

  2. Вся проверка выполняется в одном месте, а не в приложении.

Есть много статей, которые описывают, когда это пахнет кодом. Например, я вижу преимущество удаления примитивной одержимости почтовым индексом, например так:

public class Address
{
    public ZipCode ZipCode { get; set; }
}

Вот конструктор ZipCode:

public ZipCode(string value)
    {
        // Perform regex matching to verify XXXXX or XXXXX-XXXX format
        _value = value;
    }

Вы бы нарушали принцип СУХОГО, помещая эту логику проверки везде, где используется почтовый индекс.

Тем не менее, как насчет следующих объектов:

  1. Дата рождения: отметьте это больше, чем запомните и меньше, чем сегодняшняя дата.

  2. Зарплата: проверьте, что больше или равно нулю.

Вы бы создали объект DateOfBirth и объект Salary? Преимущество заключается в том, что вы можете говорить о них при описании модели предметной области. Тем не менее, является ли это случаем переобучения, так как не так много проверок. Существует ли правило, которое описывает, когда и когда не следует устранять примитивную одержимость, или вам всегда следует делать это, если это возможно?

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

w0051977
источник
8
«Вы бы нарушили принцип СУХОГО, поместив эту логику проверки везде, где используется почтовый индекс». Это неправда. Проверка должна быть сделана, как только данные введены в ваш модуль . Если существует более одной «точки входа», валидация должна быть в многократно используемой единице , и это не должно быть (и не должно быть) DTO ...
Тимоти Тракле
1
Как вы даете "mindate" и "сегодняшний день DateOfBirth" конструктору для проверки?
Caleth
11
Еще одним преимуществом создания пользовательских типов является безопасность типов. Если вы имеете Salaryи Distanceвозражаете, вы не можете случайно использовать их взаимозаменяемо. Вы можете, если они оба типа double.
Scroog1
3
@ w0051977 Ваше заявление (как я понял) подразумевает, что что-либо еще, кроме проверки в конструкторе DTO, будет нарушать DRY. На самом деле проверка должна быть за пределами DTO ...
Тимоти Траклл
2
Для меня это все вопрос масштаба. Если вы даете примитивам широкую сферу применения, то существует множество способов, которыми они могут быть использованы неправильно и неправильно. Таким образом, вы обычно хотите дать им более узкую область, и один из способов сделать это - разработать класс, представляющий концепцию, используя примитив, хранящийся в частном порядке как внутреннее, для его реализации. Теперь область действия примитива узка и вряд ли будет использоваться неправильно или неправильно, и вы можете эффективно поддерживать инварианты. Но если область действия примитива была узкой с самого начала, это может быть излишним и ввести много дополнительной связи и кода для поддержки.

Ответы:

17

Primitive Obsession использует примитивные типы данных для представления идей предметной области.

Противоположностью было бы «моделирование предметной области» или, возможно, «чрезмерное проектирование».

Вы бы создали объект DateOfBirth и объект Salary?

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

Что касается DateOfBirth, вероятно, есть две проблемы для рассмотрения. Во-первых, создание непримитивной даты дает вам возможность сосредоточить все странные проблемы на математике даты. Многие языки предоставляют один из коробки; DateTime , java.util.Date . Это независимые от домена реализации дат, но они не являются примитивами.

Во-вторых, на DateOfBirthсамом деле это не время для свиданий; здесь, в США, «дата рождения» является культурной конструкцией / юридической фикцией. Мы склонны измерять дату рождения с местной даты рождения человека; У Боба, родившегося в Калифорнии, может быть «более ранняя» дата рождения, чем у Элис, родившейся в Нью-Йорке, хотя он и младше из них.

Существует ли правило, которое описывает, когда и когда не следует устранять примитивную одержимость, или всегда следует делать это, если это возможно.

Конечно, не всегда; на границах приложения не являются объектно-ориентированными . Довольно часто встречаются примитивы, используемые для описания поведения в тестах .

VoiceOfUnreason
источник
1
Первый комментарий после цитаты вверху, похоже, не является следствием. Кроме того, это просто повторяет тему вопроса. В противном случае это хороший ответ, но я нахожу это действительно отвлекающим.
JimmyJames
ни C # DateTime, ни java.util.Date не являются подходящими базовыми типами для DateOfBirth.
Кевин Клайн
Возможно заменить java.util.Dateнаjava.time.LocalDate
Корай Тугай
7

Если честно: это зависит.

Всегда существует риск чрезмерного совершенствования вашего кода. Насколько широко будут использоваться DateOfBirth и Salary? Будете ли вы использовать их только в трех тесно связанных классах или они будут использоваться во всем приложении? Не могли бы вы «просто» инкапсулировать их в их собственный тип / класс, чтобы применить это одно ограничение, или вы можете придумать больше ограничений / функций, которые на самом деле принадлежат им?

Возьмем, к примеру, Salary: есть ли у вас какие-либо операции с «Salary» (например, обработка разных валют или, возможно, функция toString ())? Подумайте, что такое Зарплата / делает, когда вы не рассматриваете ее как простой примитив, и у Зарплаты есть хороший шанс стать своим собственным классом.

CharonX
источник
Является ли псевдоним типа хорошей альтернативой?
w0051977
@ w0051977 Я согласен с charonx, и альтернатива типа могла бы быть альтернативой
techagrammer
@ w0051977 псевдоним типа может быть альтернативой, если основная цель состоит в том, чтобы принудить строгую типизацию, четко указать, что такое определенное значение (Зарплата), чтобы избежать случайного присвоения «плавающих долларов» (за час? неделю? месяц?) «плавающая зарплата» (за месяц? год?). Это действительно зависит от ваших потребностей.
CharonX
@CharonX, я считаю, что десятичная дробь должна использоваться для зарплаты, а не для числа с плавающей запятой. Вы согласны?
w0051977
@ w0051977 Если у вас есть хороший десятичный тип, то этот будет предпочтительнее, да. (Я сейчас работаю над проектом C ++, поэтому логические, целые и плавающие числа находятся в центре моего внимания)
CharonX
5

Возможное практическое правило может зависеть от уровня программы. Для домена (DDD), также известного как уровень сущностей (Martin, 2018), это также может быть «избегать примитивов для всего, что представляет собой концепцию домена / бизнеса». Оправдания соответствуют ОП: более выразительная модель предметной области, проверка бизнес-правил, явная неявная концепция (Evans, 2004).

Псевдоним типа может быть упрощенной альтернативой (Ghosh, 2017) и при необходимости может быть изменен на класс сущности. Например, мы можем сначала потребовать, чтобы это Salaryбыло >=0, а потом решили запретить $100.33333и что-либо выше $10,000,000(что может обанкротить клиента). Использование Nonnegativeпримитива для представления Salaryи других концепций усложнит этот рефакторинг.

Избегание примитивов может также помочь избежать чрезмерной инженерии. Предположим, нам нужно объединить зарплату и дату рождения в структуру данных: например, чтобы иметь меньше параметров метода или передавать данные между модулями. Тогда мы можем использовать кортеж с типом (Salary, DateOfBirth). Действительно, кортеж с примитивами (Nonnegative, Nonnegative)неинформативен, в то время как некоторые раздутые class EmployeeDataскрывали бы обязательные поля среди других. Сигнатура в скажем calcPension(d: (Salary, DateOfBirth))более сфокусирована, чем в calcPension(d: EmployeeData), что нарушает принцип разделения интерфейса. Аналогичным образом, специалист class SalaryAndDateOfBirthкажется неловким и, вероятно, излишним. Позже мы можем определить класс данных; кортежи и элементарные типы доменов позволяют нам откладывать такие решения.

На внешнем уровне (например, GUI) может иметь смысл «обрезать» объекты до их составляющих примитивов (например, поместить в DAO). Это предотвращает утечку доменных абстракций во внешние слои, как это было рекомендовано в Martin (2018).

Ссылки
Э. Эванс, «Доменно-управляемое проектирование», 2004 г.
Д. Гош, «Функциональное и реактивное моделирование доменов», 2017
RC Martin, «Чистая архитектура», 2018 г.

Tupolev._
источник
+1 за все ссылки.
w0051977
4

Лучше страдать от Первобытной одержимости или быть астронавтом архитектуры ?

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

Как почти всегда, вы хотите умеренности, надеюсь, хорошо продуманный средний путь.

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

Deduplicator
источник
3

Если у вас был класс Salary, он мог бы иметь такие методы, как ApplyRaise.

С другой стороны, ваш класс ZipCode не должен иметь внутреннюю проверку, чтобы избежать дублирования проверки везде, где вы могли бы иметь класс ZipCodeValidator, который мог бы быть внедрен, поэтому, если ваша система должна работать как по адресам США и Великобритании, вы можете просто ввести правильный валидатор, и когда вам нужно обработать адреса AUS, вы можете просто добавить новый валидатор.

Другая проблема заключается в том, что если вам нужно записать данные в базу данных через EntityFramework, то вам необходимо знать, как обрабатывать Salary или ZipCode.

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

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

изогнутый
источник
Является ли псевдоним типа хорошей альтернативой?
w0051977
2

(Что вопрос, вероятно, на самом деле)

Когда использование примитивного типа не является запахом кода?

(Ответ)

Если у параметра нет правил - используйте примитивный тип.

Используйте примитивный тип для подобных:

htmlEntityEncode(string value)

Используйте объект для подобных:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Последний пример есть правила в нем, то есть объект SimpleDateсостоит из Year, Monthи Day. Благодаря использованию объекта в этом случае концепция SimpleDateдействительности может быть заключена в объекте.

Поднял PHP Инженер
источник
1

Помимо канонических примеров адресов электронной почты или почтовых индексов, приведенных в других местах этого вопроса, особенно полезен рефакторинг вне Primitive Obsession с идентификаторами сущностей (см. Https://andrewlock.net/using-strongly-typed-entity -ids-to-избежать-примитив-одержимость-часть-1 / для примера, как это сделать в .NET).

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

int leaveId = 12345;
int submitterId = 23456;
int approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(int leaveId, int submitterId, int approverId) {
  // implementation here
}

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

LeaveId leaveId = 12345;
SubmitterId submitterId = 23456;
ApproverId approverId = 34567;

SubmitLeaveApplication(leaveId, approverId, submitterId);

public void SubmitLeaveApplication(LeaveId leaveId, SubmitterId submitterId, ApproverId approverId) {
  // implementation here
}

Представьте себе, что этот метод масштабируется до 10 или более параметров, всех типов intданных (не говоря уже о запахе кода из длинного списка параметров ). Это становится еще хуже, когда вы используете что-то вроде AutoMapper для переключения между объектами домена и DTO, и рефакторинг, который вы делаете не улавливается автоматическим отображением.

Дэвид Кивини
источник
0

Вы бы нарушали принцип СУХОГО, помещая эту логику проверки везде, где используется почтовый индекс.

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

Но сохраняете ли вы отдельно страну как часть Address(которая также является частью почтового индекса) и часть почтового индекса (для проверки)?

  • Если вы это сделаете, вы также нарушаете DRY. Даже если вы не называете это СУХОЙ нарушением (потому что каждый экземпляр служит своей цели), он все равно неоправданно занимает дополнительную память, в дополнение к открытию двери для ошибок, когда значения двух стран различаются (что они логически никогда не должны быть).
    • Или, наоборот, это приводит к необходимости синхронизировать две точки данных, чтобы гарантировать, что они всегда одинаковы, что предполагает, что вы все равно должны действительно хранить эти данные в одной точке, что наносит ущерб цели.
  • Если вы этого не сделаете, то это не ZipCodeкласс, а Addressкласс, который снова будет содержать, string ZipCodeчто означает, что мы прошли полный круг.

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

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

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

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

Откуда я, почтовые индексы всегда числовые. Таким образом, у нас есть выбор, мы можем сохранить его как intили как string. Мы склонны использовать строку, потому что нет математических операций с данными, но бизнес-аналитик никогда не говорил мне, что это должна быть строка. Это решение остается за разработчиком (или, возможно, техническим аналитиком, хотя, по моему опыту, они не имеют прямого отношения к мелочам).

Бизнес-аналитик не заботится о типе данных, пока приложение делает то, что оно должно делать.


Валидация - сложная задача, потому что она полагается на то, чего ожидают люди.

С одной стороны, я не согласен с аргументом валидации как способом показать, почему следует избегать примитивной одержимости, потому что я не согласен с тем, что (как универсальная истина) данные всегда должны проверяться всегда.

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

В качестве второго примера, когда вы вводите свой адрес в форму, обычно вы вводите свой почтовый индекс перед вашей страной. Хотя было бы приятно получить немедленную обратную связь для проверки в пользовательском интерфейсе, на самом деле для меня (как пользователя) было бы помехой, если приложение предупредило меня о «неправильном» формате почтового индекса, поскольку реальный источник проблемы (например) заключается в том, что моя страна не является страной, выбранной по умолчанию, и, следовательно, проверка произошла не для той страны.
Это неправильное сообщение об ошибке, которое отвлекает пользователя и вызывает ненужную путаницу.

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

Дата рождения: отметьте это больше, чем запомните и меньше, чем сегодняшняя дата.
Зарплата: проверьте, что больше или равно нулю.

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

Проверка того, что дата больше, чем разум, является излишней. Ментат буквально означает, что это наименьшая возможная дата. Кроме того, где вы проводите черту актуальности? Какой смысл предотвращать, DateTime.MinDateно разрешать DateTime.MinDate.AddSeconds(1)? Вы выбираете определенную ценность, которая не особенно неправильна по сравнению со многими другими ценностями.

Мой день рождения 2 января 1978 года (это не так, но давайте предположим, что это так). Но допустим, что данные в вашем приложении неверны, и вместо этого говорится, что у меня день рождения:

  • 1 января 1978 г.
  • 1 января 1722 г.
  • 1 января 2355

Все эти даты неверны. Ни один из них не является «более правильным», чем другой. Но ваше правило проверки будет только поймать один из этих трех примеров.

Вы также полностью опустили контекст того, как вы используете эти данные. Если это используется, например, в боте с напоминанием о дне рождения, я бы сказал, что проверка бессмысленна, поскольку нет неправильных последствий для заполнения неправильной даты.
С другой стороны, если это правительственные данные и вам нужна дата рождения, чтобы подтвердить личность кого-либо (а невыполнение этого приведет к плохим последствиям, например, к отказу кого-либо в социальном обеспечении), то правильность данных имеет первостепенное значение, и вам необходимо полностью проверить данные. Предлагаемая проверка, которую вы имеете сейчас, не является адекватной.

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

Если вместо этого зарплата рассчитывается по вашему приложению, и каким-то образом можно получить отрицательное (и правильное) число, то лучшим подходом было бы сделать, Math.Max(myValue, 0)чтобы превратить отрицательные числа в 0, а не проваливать проверку. Потому что, если ваша логика решила, что результатом является отрицательное число, отказ от проверки означает, что ему придется повторить расчет, и нет никаких оснований полагать, что во второй раз он получит другое число.
И если он придет с другим числом, это снова заставит вас подозревать, что расчет не является последовательным и, следовательно, нельзя доверять.

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

Flater
источник
Дата рождения кого-то может фактически быть за текущей датой, если ребенок родился прямо сейчас в часовом поясе, который уже пропущен на следующий день. И больница может хранить «ожидаемую дату рождения» в базе данных, которая может быть месяцами в будущем. Вы хотите другой тип для этого?
gnasher729
@ gnasher729: Я не совсем уверен, что следую, кажется, вы согласны со мной (проверка является контекстуальной и не всегда корректной), но формулировка вашего комментария предполагает, что вы думаете, что я не согласен. Или я неправильно читаю?
Флэтер