惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
GbyAI
GbyAI
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
N
Netflix TechBlog - Medium
G
Google Developers Blog
博客园_首页
有赞技术团队
有赞技术团队
V
V2EX
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
4. Проектирование с помощью типов: Исследуем новые концепции
markshevchen · 2026-04-28 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение5 мин

Охват и читатели441

Туториал

Перевод

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

Правило звучало так: «У контакта должен быть электронный или почтовый адрес».

А тип, который мы спроектировали, получился таким:

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" postalAddress

let 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 PhoneContactInfo

type 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" phoneNumber

let 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) обсуждению важности углубленного рефакторинга.

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

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