Недавно я прочитал множество статей, в которых примитивная одержимость описывается как запах кода.
Есть два преимущества избегания примитивной одержимости:
Это делает модель предметной области более явной. Например, я могу поговорить с бизнес-аналитиком о почтовом индексе вместо строки, содержащей почтовый индекс.
Вся проверка выполняется в одном месте, а не в приложении.
Есть много статей, которые описывают, когда это пахнет кодом. Например, я вижу преимущество удаления примитивной одержимости почтовым индексом, например так:
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;
}
Вы бы нарушали принцип СУХОГО, помещая эту логику проверки везде, где используется почтовый индекс.
Тем не менее, как насчет следующих объектов:
Дата рождения: отметьте это больше, чем запомните и меньше, чем сегодняшняя дата.
Зарплата: проверьте, что больше или равно нулю.
Вы бы создали объект DateOfBirth и объект Salary? Преимущество заключается в том, что вы можете говорить о них при описании модели предметной области. Тем не менее, является ли это случаем переобучения, так как не так много проверок. Существует ли правило, которое описывает, когда и когда не следует устранять примитивную одержимость, или вам всегда следует делать это, если это возможно?
Я думаю, я мог бы создать псевдоним типа вместо класса, который помог бы с пунктом 1 выше.
DateOfBirth
" конструктору для проверки?Salary
иDistance
возражаете, вы не можете случайно использовать их взаимозаменяемо. Вы можете, если они оба типаdouble
.Ответы:
Противоположностью было бы «моделирование предметной области» или, возможно, «чрезмерное проектирование».
Введение объекта Salary может быть хорошей идеей по следующей причине: числа редко бывают самостоятельными в модели предметной области, они почти всегда имеют измерение и единицу. Обычно мы не моделируем ничего полезного, если добавляем длину ко времени или массе, и мы редко получаем хорошие результаты, когда мы смешиваем метры и ноги.
Что касается DateOfBirth, вероятно, есть две проблемы для рассмотрения. Во-первых, создание непримитивной даты дает вам возможность сосредоточить все странные проблемы на математике даты. Многие языки предоставляют один из коробки; DateTime , java.util.Date . Это независимые от домена реализации дат, но они не являются примитивами.
Во-вторых, на
DateOfBirth
самом деле это не время для свиданий; здесь, в США, «дата рождения» является культурной конструкцией / юридической фикцией. Мы склонны измерять дату рождения с местной даты рождения человека; У Боба, родившегося в Калифорнии, может быть «более ранняя» дата рождения, чем у Элис, родившейся в Нью-Йорке, хотя он и младше из них.Конечно, не всегда; на границах приложения не являются объектно-ориентированными . Довольно часто встречаются примитивы, используемые для описания поведения в тестах .
источник
java.util.Date
наjava.time.LocalDate
Если честно: это зависит.
Всегда существует риск чрезмерного совершенствования вашего кода. Насколько широко будут использоваться DateOfBirth и Salary? Будете ли вы использовать их только в трех тесно связанных классах или они будут использоваться во всем приложении? Не могли бы вы «просто» инкапсулировать их в их собственный тип / класс, чтобы применить это одно ограничение, или вы можете придумать больше ограничений / функций, которые на самом деле принадлежат им?
Возьмем, к примеру, Salary: есть ли у вас какие-либо операции с «Salary» (например, обработка разных валют или, возможно, функция toString ())? Подумайте, что такое Зарплата / делает, когда вы не рассматриваете ее как простой примитив, и у Зарплаты есть хороший шанс стать своим собственным классом.
источник
Возможное практическое правило может зависеть от уровня программы. Для домена (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 г.
источник
Лучше страдать от Первобытной одержимости или быть астронавтом архитектуры ?
Оба случая являются патологическими, в одном случае у вас слишком мало абстракций, что приводит к повторению и легко принимает яблоко за апельсин, а в другом вы уже забыли покончить с этим и начать делать что-то, затрудняя выполнение чего-либо. ,
Как почти всегда, вы хотите умеренности, надеюсь, хорошо продуманный средний путь.
Помните, что у свойства есть имя в дополнение к типу. Кроме того, декомпозиция адреса на составные части может быть слишком сужающей, если всегда выполняется одинаково. Не весь мир находится в центре Нью-Йорка.
источник
Если у вас был класс Salary, он мог бы иметь такие методы, как ApplyRaise.
С другой стороны, ваш класс ZipCode не должен иметь внутреннюю проверку, чтобы избежать дублирования проверки везде, где вы могли бы иметь класс ZipCodeValidator, который мог бы быть внедрен, поэтому, если ваша система должна работать как по адресам США и Великобритании, вы можете просто ввести правильный валидатор, и когда вам нужно обработать адреса AUS, вы можете просто добавить новый валидатор.
Другая проблема заключается в том, что если вам нужно записать данные в базу данных через EntityFramework, то вам необходимо знать, как обрабатывать Salary или ZipCode.
Нет четкого ответа о том, где провести черту между тем, какими должны быть интеллектуальные классы, но я скажу, что я склонен переносить бизнес-логику, например валидацию, в классы бизнес-логики, в которых классы данных являются чистыми данными, как это кажется работать лучше с EntityFramework.
Что касается использования псевдонимов типов, имя члена / свойства должно содержать всю необходимую информацию о содержимом, поэтому я не буду использовать псевдонимы типов.
источник
(Что вопрос, вероятно, на самом деле)
Когда использование примитивного типа не является запахом кода?
(Ответ)
Если у параметра нет правил - используйте примитивный тип.
Используйте примитивный тип для подобных:
Используйте объект для подобных:
Последний пример есть правила в нем, то есть объект
SimpleDate
состоит изYear
,Month
иDay
. Благодаря использованию объекта в этом случае концепцияSimpleDate
действительности может быть заключена в объекте.источник
Помимо канонических примеров адресов электронной почты или почтовых индексов, приведенных в других местах этого вопроса, особенно полезен рефакторинг вне Primitive Obsession с идентификаторами сущностей (см. Https://andrewlock.net/using-strongly-typed-entity -ids-to-избежать-примитив-одержимость-часть-1 / для примера, как это сделать в .NET).
Я потерял счетчик количества случаев, когда ошибка появлялась, потому что метод имел подпись, подобную этой:
Компилируется просто отлично, и если вы не проводите строгие тесты, это тоже может пройти. Тем не менее, преобразовать эти идентификаторы сущностей в классы, специфичные для домена, и, кстати, ошибки времени компиляции:
Представьте себе, что этот метод масштабируется до 10 или более параметров, всех типов
int
данных (не говоря уже о запахе кода из длинного списка параметров ). Это становится еще хуже, когда вы используете что-то вроде AutoMapper для переключения между объектами домена и DTO, и рефакторинг, который вы делаете не улавливается автоматическим отображением.источник
С другой стороны, когда речь идет о многих разных странах и их разных системах почтовых индексов, это означает, что вы не можете проверить почтовый индекс, если не знаете соответствующую страну. Таким образом, ваш
ZipCode
класс должен также хранить страну.Но сохраняете ли вы отдельно страну как часть
Address
(которая также является частью почтового индекса) и часть почтового индекса (для проверки)?ZipCode
класс, аAddress
класс, который снова будет содержать,string ZipCode
что означает, что мы прошли полный круг.Я не понимаю вашего основного утверждения, что когда часть информации имеет заданный тип переменной, вы как-то обязаны упоминать этот тип всякий раз, когда разговариваете с бизнес-аналитиком.
Зачем? Почему вы не можете просто поговорить о «почтовом индексе» и полностью опустить конкретный тип? Какие обсуждения вы ведете со своим бизнес-аналитиком (не техническим!), Когда тип собственности является наиболее важным для разговора?
Откуда я, почтовые индексы всегда числовые. Таким образом, у нас есть выбор, мы можем сохранить его как
int
или какstring
. Мы склонны использовать строку, потому что нет математических операций с данными, но бизнес-аналитик никогда не говорил мне, что это должна быть строка. Это решение остается за разработчиком (или, возможно, техническим аналитиком, хотя, по моему опыту, они не имеют прямого отношения к мелочам).Бизнес-аналитик не заботится о типе данных, пока приложение делает то, что оно должно делать.
Валидация - сложная задача, потому что она полагается на то, чего ожидают люди.
С одной стороны, я не согласен с аргументом валидации как способом показать, почему следует избегать примитивной одержимости, потому что я не согласен с тем, что (как универсальная истина) данные всегда должны проверяться всегда.
Например, что если это более сложный поиск? Вместо простой проверки формата, что если ваша проверка влечет за собой обращение к внешнему API и ожидание ответа? Вы действительно хотите заставить свое приложение вызывать этот внешний API для каждого
ZipCode
объекта, который вы создаете?Может быть, это строгое требование бизнеса, и тогда оно, конечно, оправдано. Но это не универсальная правда. Там будет много случаев использования, где это больше бремя, чем решение.
В качестве второго примера, когда вы вводите свой адрес в форму, обычно вы вводите свой почтовый индекс перед вашей страной. Хотя было бы приятно получить немедленную обратную связь для проверки в пользовательском интерфейсе, на самом деле для меня (как пользователя) было бы помехой, если приложение предупредило меня о «неправильном» формате почтового индекса, поскольку реальный источник проблемы (например) заключается в том, что моя страна не является страной, выбранной по умолчанию, и, следовательно, проверка произошла не для той страны.
Это неправильное сообщение об ошибке, которое отвлекает пользователя и вызывает ненужную путаницу.
Точно так же, как вечная проверка не является универсальной правдой, так же как и мои примеры. Это контекстно . Некоторые домены приложений требуют проверки данных превыше всего. Другие домены не ставят валидацию так высоко в списке приоритетов, потому что хлопоты, которые он приносит, конфликтуют с их фактическими приоритетами (например, пользовательским интерфейсом или способностью изначально хранить ошибочные данные, чтобы их можно было исправить, вместо того, чтобы никогда не позволять им быть сохранены)
Проблема с этими проверками заключается в том, что они являются неполными, избыточными или указывают на гораздо большую проблему .
Проверка того, что дата больше, чем разум, является излишней. Ментат буквально означает, что это наименьшая возможная дата. Кроме того, где вы проводите черту актуальности? Какой смысл предотвращать,
DateTime.MinDate
но разрешатьDateTime.MinDate.AddSeconds(1)
? Вы выбираете определенную ценность, которая не особенно неправильна по сравнению со многими другими ценностями.Мой день рождения 2 января 1978 года (это не так, но давайте предположим, что это так). Но допустим, что данные в вашем приложении неверны, и вместо этого говорится, что у меня день рождения:
Все эти даты неверны. Ни один из них не является «более правильным», чем другой. Но ваше правило проверки будет только поймать один из этих трех примеров.
Вы также полностью опустили контекст того, как вы используете эти данные. Если это используется, например, в боте с напоминанием о дне рождения, я бы сказал, что проверка бессмысленна, поскольку нет неправильных последствий для заполнения неправильной даты.
С другой стороны, если это правительственные данные и вам нужна дата рождения, чтобы подтвердить личность кого-либо (а невыполнение этого приведет к плохим последствиям, например, к отказу кого-либо в социальном обеспечении), то правильность данных имеет первостепенное значение, и вам необходимо полностью проверить данные. Предлагаемая проверка, которую вы имеете сейчас, не является адекватной.
Для зарплаты есть здравый смысл: она не может быть отрицательной. Но если вы реально ожидаете, что вводятся бессмысленные данные, я бы посоветовал вам изучить источник этих бессмысленных данных. Потому что, если им нельзя доверять ввод чувствительных данных, вы также не можете доверять им ввод правильных данных.
Если вместо этого зарплата рассчитывается по вашему приложению, и каким-то образом можно получить отрицательное (и правильное) число, то лучшим подходом было бы сделать,
Math.Max(myValue, 0)
чтобы превратить отрицательные числа в 0, а не проваливать проверку. Потому что, если ваша логика решила, что результатом является отрицательное число, отказ от проверки означает, что ему придется повторить расчет, и нет никаких оснований полагать, что во второй раз он получит другое число.И если он придет с другим числом, это снова заставит вас подозревать, что расчет не является последовательным и, следовательно, нельзя доверять.
Это не значит, что проверка бесполезна. Но бессмысленная проверка - это плохо, потому что она требует усилий, но в действительности не решает проблему, и дает людям ложное чувство безопасности.
источник