Примитив против класса для представления простого объекта домена?


14

Каковы общие рекомендации или практические правила, когда следует использовать объект, специфичный для домена, против простой строки или числа?

Примеры:

  • Возрастной класс против Integer?
  • FirstName класс против строки?
  • UniqueID против строки
  • Класс PhoneNumber vs String vs Long?
  • Класс DomainName против строки?

Я думаю, что большинство практиков ООП определенно сказали бы определенные классы для PhoneNumber и DomainName. Чем больше правил относительно того, что делает их действительными и как их сравнивать, тем проще и безопаснее работать с простыми классами. Но для первых трех есть больше дебатов.

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

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

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

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


2
Возрастной класс не очень хорошая идея, если он может быть заменен целым числом. Если он принимает дату рождения в конструкторе и имеет методы getCurrentAge () и getAgeAt (Date date), тогда класс имеет значение. Но его нельзя заменить целым числом.
Кивирон

5
Msgstr "Строка иногда не является строкой. Смоделируйте её соответственно." Есть хорошая запись Марка Симанна о «примитивной одержимости»: blog.ploeh.dk/2015/01/19/…
Сергей Шушляпин

4
На самом деле класс Age имеет какой-то странный смысл. Распространенное западное определение возраста - ноль при рождении и добавление 1 к вашему следующему дню рождения. Восточноазиатская методология заключается в том, что вам 1 год при рождении, а 1 добавляется в следующий новогодний день. Таким образом, в зависимости от вашего региона ваш возраст может быть рассчитан по-разному. EG от en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
Peter M

@PeterM, это хорошая информация! Даты / времена, а теперь и возраст. Они должны быть простыми понятиями, но это доказывает, что в мире гораздо больше сложности, чем то, с чем мы привыкли справляться.
Мачадо

6
Я вижу много написания ниже этого вопроса, но ответ не так уж и сложен. Вы используете Ageкласс вместо целого числа, когда вам нужно дополнительное поведение, которое обеспечит такой класс.
Роберт Харви

Ответы:


20

Каковы общие рекомендации или практические правила, когда следует использовать объект, специфичный для домена, против простой строки или числа?

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

Подумайте: почему мы используем целое число? Мы можем так же легко представить все целые числа в виде строк. Или с байтами.

Если бы мы программировали на языке, независимом от предметной области, который включал примитивные типы для целого числа и возраста, что бы вы выбрали?

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

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

Различение типов позволяет нам моделировать различия между непроверенным адресом электронной почты и проверенным адресом электронной почты .

Случайность того, как эти значения представлены в памяти, является одной из наименее интересных частей. Доменная модель не заботится о CustomerIdтипе int, или String, или UUID / GUID, или узле JSON. Он просто хочет денег.

Действительно ли нам важно, являются ли целые числа старшими или младшими? Неужели нас волнует, что Listмы передали абстракцию над массивом или граф? Когда мы обнаружим, что арифметика двойной точности неэффективна, и что нам нужно перейти к представлению с плавающей запятой, должна ли заботиться модель предметной области ?

Парнас в 1972 году написал

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

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

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

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

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


Отличное замечание о сокрытии (или абстрагировании) тех вещей, которые трудно изменить и которые могут измениться.
user949300

4

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

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

Если у вас есть эта абстракция, вам необязательно нужны дополнительные абстракции для атрибутов, которые являются просто значениями. Класс Person может содержать BirthDate как (примитивную) Date вместе с PhoneNumbers как список (примитивных) строк и Name как (примитивную) строку.

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

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


1

Вы должны моделировать так, как вам нужно, если вы просто хотите получить некоторые данные, вы можете использовать типы примитивов, но если вы хотите выполнить больше операций с этими данными, они должны быть смоделированы в определенных классах, например (я согласен с @kiwiron в его комментарии) Возрастной класс не имеет смысла, но было бы лучше заменить его классом Birthdate, и вы поместите эти методы там.

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

Для Firstname я не вижу большой пользы, UniqueID тоже немного PhoneNumber, вы можете попросить его указать вам страну и регион, например, потому что в общем PhoneNumber относится к странам и регионам, некоторые операторы используют предопределенные суффиксы номеров для классификации Mobile , Office ... числа, так что вы можете добавить эти методы в этот класс, то же самое для DomainName.

Для получения более подробной информации я рекомендую вам взглянуть на объекты значения в DDD (Domain Driven Design).


1

Это зависит от того, есть ли у вас поведение (которое вам нужно поддерживать и / или изменить), связанное с этими данными, или нет.

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

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


1

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

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

Как отмечали другие, возраст может быть сложным предметом в зависимости от вашего региона. Если вам нужно согласовать возраст в разных локалях и т. Д., Тогда имеет смысл ввести Ageкласс, который имеет свойства DateOfBirthи Localeкак. Этот возрастной класс также может рассчитывать текущий возраст в любой заданной отметке времени.

Таким образом, есть причины для продвижения примитива в классе, но они очень специфичны для приложения. Вот некоторые примеры:

  • У вас есть специальная логика проверки, которая должна применяться везде, где используется тип информации (например, номера телефонов, адреса электронной почты).
  • У вас есть специальная логика форматирования, которая используется для визуализации для чтения людьми (например, адреса IP4 и IP6)
  • У вас есть набор функций, которые все относятся к этому типу объекта
  • У вас есть специальные правила для сравнения похожих типов значений

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


1

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

Примитив intозначает, что какой-то процесс обнулил 4 байта памяти, а затем перевернул биты, необходимые для представления некоторого целого числа в диапазоне от -2 147 483 648 до 2 147 483 647; что не является сильным утверждением о концепции возраста. Аналогично, (не нуль) System.Stringозначает, что некоторые байты были выделены, и биты были перевернуты, чтобы представить последовательность символов Unicode относительно некоторой кодировки.

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

Точно так же Ageобъект, вероятно, должен быть свидетелем того, что какой-то процесс вычислил разницу между двумя датами. Поскольку, intsкак правило, не являются результатом вычисления разницы между двумя датами, они ipso facto недостаточны для представления возраста.

Таким образом, совершенно очевидно, что вы почти всегда хотите создать класс (который является просто программой) для каждого объекта домена. Так в чем проблема? Проблема в том, что C # (который я люблю) и Java требуют слишком много церемоний при создании классов. Но мы можем представить альтернативные синтаксисы, которые сделали бы определение предметных объектов гораздо более простым:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

F #, например, на самом деле имеет хороший синтаксис для этого в виде активных шаблонов :

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

Дело в том, что если ваш язык программирования затрудняет определение объектов домена, это еще не значит, что на самом деле это плохая идея.


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


0

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

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

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

Также поймите, что иногда хорошее именование может справиться с этим (т. Е. Строка с именем firstNameили создание целого класса, который просто оборачивает свойство строки). Занятия не единственный способ решить проблемы.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.