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


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;
    }

Ви б порушили принцип DRY, поставивши, що логіка перевірки скрізь використовується поштовий індекс.

Однак як щодо наступних об'єктів:

  1. Дата народження: Перевірте, чи більший за минулий та менший за сьогоднішній день.

  2. Зарплата: Перевірте, що більше або дорівнює нулю.

Чи створили б ви об'єкт DateOfBirth та об'єкт заробітної плати? Перевага полягає в тому, що ви можете говорити про них під час опису доменної моделі. Однак, це випадок переобладнання, оскільки валідації не багато. Чи є правило, яке описує, коли і коли не знімати примітивні нав'язливості чи завжди ти повинен це робити, якщо можливо?

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


8
"Ви б порушили принцип DRY, поставивши, що логіка перевірки скрізь використовується поштовий індекс." Це не правда. Валідацію слід зробити, як тільки дані будуть введені у ваш модуль . Якщо є декілька "точок входу", перевірка повинна бути в одиниці багаторазового використання , і це не потрібно (або не повинно бути) DTO ...
Тімоті Вірле,

1
Як ви даєте "mindate" і "сьогоднішню дату" DateOfBirthконструктору, щоб він перевірив?
Калет

11
Ще одна перевага створення спеціальних типів - безпека типу. Якщо у вас є Salaryі Distanceоб'єкт , який ви не можете випадково використовувати їх як взаємозамінні. Ви можете, якщо вони обидва типу double.
Scroog1

3
@ w0051977 Ви заява (як я це зрозумів) мав на увазі, що будь-що інше, ніж перевірка в конструкторі DTO, порушить DRY. Насправді перевірка повинна бути поза межами
ДТО

2
Для мене це все питання сфери. Якщо ви даєте примітивам широку сферу застосування, то існує чимало способів, якими вони можуть бути неправомірно використані та неправомірні. Таким чином, ви, як правило, хочете надати їм більш вузьку сферу, і один із способів зробити це - розробити клас, що представляє концепцію, використовуючи примітивний, приватно збережений як внутрішній, для його реалізації. Зараз сфера застосування примітиву вузька і навряд чи буде нецільовим шляхом чи неправильним поводженням, і ви зможете ефективно підтримувати інваріанти. Але якби сфера примітиву для початку була вузькою, це може бути зайвим і ввести багато додаткового зв'язку та коду для підтримки.

Відповіді:


17

Primitive Obsession використовує примітивні типи даних для представлення ідей домену.

Протилежним було б "моделювання домену", або, можливо, "над інженерією".

Чи створили б ви об'єкт DateOfBirth та об'єкт заробітної плати?

Введення об’єкта заробітної плати може бути хорошою ідеєю з наступної причини: числа рідко стоять окремо у доменній моделі, вони майже завжди мають розмірність та одиницю. Зазвичай ми не моделюємо нічого корисного, якщо додаємо довжину часу або маси, і рідко отримуємо хороші результати, коли ми змішуємо метри і ноги.

Що стосується DateOfBirth, напевно - слід розглянути два питання. По-перше, створення непримітивної дати дає вам змогу зосередити всі дивні проблеми щодо математики дати. Багато мов пропонують один із вікон; DateTime , java.util.Date . Це доменні агностичні реалізації дат, але вони не є примітивними.

По-друге, DateOfBirthнасправді не час побачення; тут, у США, "дата народження" - це культурний конструкт / юридична вигадка. Ми схильні вимірювати дату народження від місцевої дати народження осіб; У Боб, який народився в Каліфорнії, може бути "раніше" дата народження, ніж Еліс, яка народилася в Нью-Йорку, навіть якщо він молодший з двох.

Чи є правило, яке описує, коли і коли не знімати примітивні нав'язливості чи слід завжди робити це, якщо можливо.

Звичайно, не завжди; на кордонах програми не орієнтовані на об'єкти . Досить часто бачити примітивів, які використовуються для опису поведінки в тестах .


1
Перший коментар після цитати вгорі, здається, не є послідовним. Крім того, він просто переновлює тему питання. Це є гарною відповіддю інакше, але я вважаю це справді відволікаючим.
JimmyJames

ні C # DateTime, ні java.util.Date не є відповідними базовими типами для DateOfBirth.
Кевін Клайн

Може замінити java.util.Dateнаjava.time.LocalDate
Koray Tugay

7

Якщо чесно: це залежить.

Завжди є ризик переобладнати код. Наскільки широко використовуватимуться DateOfBirth та зарплата? Чи будете ви використовувати їх лише у трьох щільно зв'язаних класах, або вони будуть використовуватися у всьому додатку? Ви б "просто" включили їх у свій Тип / Клас, щоб застосувати це обмеження, або ви можете придумати більше обмежень / функцій, які насправді там належать?

Візьмемо для прикладу Заробітну плату: Чи є у вас операції з "Заробітною платою" (наприклад, обробка різних валют, або, можливо, функція toString ())? Поміркуйте, що таке / чи є заробітна плата, коли ви не дивитесь на це як на простого примітиву, і є хороший шанс для зарплати бути власним класом.


Чи є псевдонім типу гарною альтернативою?
w0051977

@ w0051977 Я погоджуюся з charonx, і псевдонім типу може бути альтернативою
techagrammer

@ w0051977 псевдонім типу може бути альтернативою, якщо основною метою є накладення суворого набору тексту, чітко вказати, що таке певне значення (зарплата), щоб уникнути випадкового присвоєння "плаваючих доларів" (за годину? тиждень? місяць?) "плаваюча зарплата" (за місяць? рік?). Це дійсно залежить від ваших потреб.
CharonX

@CharonX, я вважаю, що десяткові кошти повинні використовуватися для зарплати, а не для плавання. Ви згодні?
w0051977

@ w0051977 Якщо у вас хороший десятковий тип, тоді це було б кращим, так. (Я зараз працюю над проектом C ++, тому булі, цілі числа та поплавці стоять в авангарді моєї думки)
CharonX

5

Можливе правило може залежати від шару програми. Для домену (DDD) aka Entities Layer (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здається незручним і, ймовірно, зайвим. Пізніше ми можемо вибрати клас даних; кортежі та елементарні типи доменів дозволяють нам відкладати такі рішення.

У зовнішньому шарі (наприклад, графічному інтерфейсі) може бути сенс "знімати" сутності до їх складових примітивів (наприклад, вводити в DAO). Це запобігає витіканню абстракцій домену у зовнішні шари, про що йдеться у Martin (2018).

Список літератури
Е. Еванс, «Дизайн, керований доменом», 2004 р.
Д. Гош, «Функціональне та реактивне моделювання доменів», 2017 р.
RC Martin, «Чиста архітектура», 2018 р.


+1 для всіх посилань.
w0051977

4

Краще страждати від примітивної одержимості чи бути космонавтом архітектури ?

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

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

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


3

Якщо у вас був клас заробітної плати, він може мати такі методи, як ApplyRaise.

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

Інша проблема - якщо вам потрібно записати дані в базу даних через EntityFramework, тоді потрібно знати, як обробляти зарплату чи ZipCode.

Не існує чітко визначеної відповіді, де провести межу між тим, наскільки інтелектуальними повинні бути класи, але я скажу, що я схильний рухатись до бізнес-логіки, як-от перевірка, до класів бізнес-логіки, у яких класи даних є чистими даними, як це здається краще працювати з EntityFramework.

Що стосується використання псевдонімів типу, ім'я члена / властивості повинно містити всю інформацію, необхідну щодо вмісту, тому я не буду використовувати псевдоніми типів.


Чи є псевдонім типу гарною альтернативою?
w0051977

2

(Яке питання, мабуть, є насправді)

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

(Відповідь)

Якщо параметр не має в ньому правил - використовуйте примітивний тип.

Використовуйте примітивний тип на зразок:

htmlEntityEncode(string value)

Використовуйте об'єкт на зразок:

numberOfDaysSinceUnixEpoch(SimpleDate value)

Останній приклад є правила в ньому, тобто об'єкт SimpleDateскладається з Year, Monthі Day. Завдяки використанню Object у цьому випадку поняття SimpleDateдійсності може бути інкапсульовано всередині об'єкта.


1

Окрім канонічних прикладів адрес електронної пошти чи поштових індексів, наведених у цьому запитанні десь, де мені здається, що рефакторинг далеко від примітивної одержимості може бути особливо корисним, це ідентифікатори юридичних осіб (див. Https://andrewlock.net/using-strongly-typed-entity -ids-to-Avoid-primitive-obsession-part-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

Ви б порушили принцип DRY, поставивши, що логіка перевірки скрізь використовується поштовий індекс.

З іншого боку, якщо ви маєте справу з багатьма різними країнами та їх різними системами поштових індексів, це означає, що ви не можете перевірити поштовий індекс, якщо ви не знаєте про цю країну. Тож ваш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, а не провал перевірки. Тому що, якщо ваша логіка вирішила, що результат є від'ємним числом, невдача перевірки означає, що доведеться повторити обчислення, і немає підстав думати, що він з’явиться з іншим числом вдруге.
І якщо все-таки з’явиться інша кількість, це знову приводить вас до підозри, що підрахунок не є послідовним і тому не можна довіряти.

Це не означає, що перевірка не корисна. Але безглузда перевірка погана, як тому, що вона вимагає зусиль, не реально вирішуючи проблему, і надаючи людям помилкове почуття безпеки.


Хтось дата народження може насправді перевершити поточну дату, якщо дитина народилася прямо зараз в часовому поясі, який вже перейшов на наступний день. І лікарня може зберігати "очікувану дату народження" в базі даних, яка може бути місяцями в майбутньому. Ви хочете для цього іншого типу?
gnasher729

@ gnasher729: Я не зовсім впевнений, що слідкую за цим, здається, ви погоджуєтесь зі мною (перевірка є контекстуальною і не є універсальною), але фразування вашого коментаря говорить про те, що ви думаєте, що я не згоден. Або я неправильно читаю?
Flater
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.