В прошлом посте мы разобрались, как, используя типы, можно представить бизнес-правило.
Правило звучало так: «У контакта должен быть электронный или почтовый адрес».
А тип, который мы спроектировали, получился таким:
type ContactInfo = | EmailOnly of EmailContactInfo | PostOnly of PostalContactInfo | EmailAndPost of EmailContactInfo * PostalContactInfo
Теперь представим, что бизнес решил добавить номера телефонов. Новое бизнес-правило: «У контакта должно быть как минимум что-то из перечисленного: электронный адрес, почтовый адрес, домашний телефон или рабочий телефон».
Как нам представить эту логику?
Немного поразмыслив, приходим к выводу, что существует 15 возможных комбинаций этих четырёх способов контакта. Мы ведь не хотим создавать объединение с 15-ю вариантами выбора? Есть ли лучший способ?
Давайте пока зафиксируем эту мысль и посмотрим на другую — связанную — проблему.
Изменение требований должно «ломать» код
А проблема такая. Представим, что у нас есть структура для представления контакта, которая содержит список электронных адресов и список почтовых адресов:
type ContactInformation = { EmailAddresses : EmailContactInfo list; PostalAddresses : PostalContactInfo list }
Представим также, что вы создали функцию printReport, которая перебирает всю информацию и печатает её в отчёте:
// фиктивный кодlet printEmail emailAddress = printfn "Email Address is %s" emailAddress// фиктивный кодlet printPostalAddress postalAddress = printfn "Postal Address is %s" postalAddresslet printReport contactInfo = let { EmailAddresses = emailAddresses; PostalAddresses = postalAddresses; } = contactInfo for email in emailAddresses do printEmail email for postalAddress in postalAddresses do printPostalAddress postalAddress
Грубо, но просто и понятно.
Теперь, если в силу вступает новое бизнес-правило, мы можем решить изменить структуру, чтобы в ней появились новые списки для телефонных номеров. Обновлённая структура будет выглядеть примерно так:
type PhoneContactInfo = string // на данный момент заглушкаtype ContactInformation = { EmailAddresses : EmailContactInfo list; PostalAddresses : PostalContactInfo list; HomePhones : PhoneContactInfo list; WorkPhones : PhoneContactInfo list; }
Сделав это изменение, надо убедиться, что в код всех функций, которые обрабатывают контактную информацию, внесены изменения, касающиеся новых телефонных полей.
Безусловно, вам придётся исправить любые сломавшиеся сопоставления с образцом. Но во многих случаях вы даже не узнаете о необходимости учесть новые варианты.
Например, вот printReport, умеющая работать с новыми списками:
let printReport contactInfo = let { EmailAddresses = emailAddresses; PostalAddresses = postalAddresses; } = contactInfo for email in emailAddresses do printEmail email for postalAddress in postalAddresses do printPostalAddress postalAddress
Видите, какую ошибку я сделал? Да, я забыл дописать функцию, чтобы она обрабатывала телефоны. Новые поля в записи вообще не ломают код. И нет никаких гарантий, что вы вспомните об обработке новых вариантов. Об этом слишком легко забыть.
И снова перед нами проблема: можно ли спроектировать типы так, чтобы обезопасить себя от подобных ситуаций?
Углублённое понимание предметной области
Если вы ещё немного подумаете над нашим примером, то обнаружите, что за деревьями мы не увидели леса.
Нашей первоначальной концепцией была: «для связи с заказчиком, у нас будет список возможных электронных адресов, список возможных почтовых адресов и т. д.».
Но на самом деле всё это неправильно. Гораздо более подходящая концепция: «для связи с заказчиком, у нас будет список способов контакта, где способ контакта — это электронный адрес ИЛИ почтовый адрес ИЛИ телефонный номер».
Это — ключевая идея, как моделировать предметную область. Она приводит нас к созданию нового типа ContactMethod, который одним махом решает наши проблемы.
Давайте подправим типы так, чтобы применить новую концепцию:
type ContactMethod = | Email of EmailContactInfo | PostalAddress of PostalContactInfo | HomePhone of PhoneContactInfo | WorkPhone of PhoneContactInfotype ContactInformation = { ContactMethods : ContactMethod list; }
Теперь код отчёта должен быть изменён, чтобы обрабатывать новый тип:
// фиктивный кодlet printContactMethod cm = match cm with | Email emailAddress -> printfn "Электронный адрес %s" emailAddress | PostalAddress postalAddress -> printfn "Почтовый адрес %s" postalAddress | HomePhone phoneNumber -> printfn "Домашний телефон %s" phoneNumber | WorkPhone phoneNumber -> printfn "Рабочий телефон %s" phoneNumberlet printReport contactInfo = let { ContactMethods=methods; } = contactInfo methods |> List.iter printContactMethod
Эти правки имеют ряд преимуществ.
Для начала, с точки зрения моделирования, новые типы гораздо лучше представляют предметную область и больше приспособлены к изменению требований.
А с точки зрения разработки, изменение типа на объединение означает, что все новые варианты, которые мы добавим (или удалим), очевидным образом сломают код, и будет гораздо сложнее случайно забыть обработать все варианты.
Возвращаемся к бизнес-правилу с 15-ю комбинациями
Вернёмся к исходному примеру. Тогда мы решили, что для кодирования бизнес-правила нам, возможно, придётся описать все 15 возможных комбинаций способов связи.
Однако, новый взгляд на проблемы с отчётом, также влияет на наше понимание бизнес-правила.
Держа в голове концепцию «способа связи», мы можем перефразировать требование следующим образом: «У заказчика должен быть как минимум один способ связи, который может быть электронным адресом ИЛИ почтовым адресом ИЛИ телефонным номером».
Перепроектируем тип Contact так, чтобы он содержал список способов связи.
type Contact = { Name: PersonalName; ContactMethods: ContactMethod list; }
Но это всё ещё не совсем верно. Список может быть пустым. Как мы можем обеспечить соблюдение правила, согласно которому должен быть как минимум один способ связи?
Простейший способ заключается в том, чтобы добавить новое обязательное поле:
type Contact = { Name: PersonalName; PrimaryContactMethod: ContactMethod; SecondaryContactMethods: ContactMethod list; }
В этом дизайне поле PrimaryConactMethod (основной способ связи) является обязательным, а дополнительные способы связи — нет, что полностью соответствует бизнес-правилу!
Кроме того, этот рефакторинг дал нам новое понимание. Вполне возможно, что концепция «основного» и «дополнительного» способа связи может, в свою очередь, прояснить код в других местах, вызвав каскадные изменения в понимании и рефакторинге.
Заключение
В этом посте мы познакомились с тем, как использование типов для моделирования бизнес-правил может помочь вам разобраться в предметной области.
В книге Предметно-ориентированное проектирование Эрик Эванс посвятил целый раздел и, в частности, две главы (8 и 9) обсуждению важности углубленного рефакторинга.
Пример в этом посте сравнительно прост, но, я надеюсь, он показывает, как углубленное понимание может помочь улучшить как модель, так и правильность кода.
В следующем посте мы узнаем, как типы помогают в представлении детальных состояний.
ссылка на оригинал статьи https://habr.com/ru/articles/1028876/