Комментарии 27
Спасибо!
Не проще ли обработать это в конструкторе класса?
Возможно, и проще, но тогда ошибка будет проявляться во время исполнения, а здесь — во время компиляции. Вероятно, обработка ошибки в конструкторе потребует также дополнительных тестов, которые тоже надо учитывать при оценке сложности того или иного решения.
Так можно просто конструкторы сделать типа contactWithEmail и contactWithPostal — и тогда физически не получится создать Contact с обоими заполненными полями.
Всё равно тогда придётся учитывать случай с некорректным состоянием, по крайней мере, при использовании сопоставления с образцом. Объявление Contact
будет проще, но использование сложнее.
К тому же, если правила поменяются и контакт без адресов станет возможен, то старый код не сломается. Конечно, напрямую такую эволюцию представить сложно, а как серию изменений — вполне:
- Приходит изменение: теперь у всех конатктов должен быть телефон и хотя бы один адрес для писем. Везде добавляется обработка телефона. При этом "невозможный" обработчик, где есть телефон, но нет ни электронной, ни обычной почты, остаётся.
- Новое изменение — теперь контакт с одним только телефоном возможен. Теперь "невозможные" обработчики могут отработать и их надо искать и обновлять.
При явном выделении состояний такой проблемы не возникнет, никаких "невозможных" обработчиков не будет, просто добавится новое состояние PhoneOnly и компилятор подскажет, где надо добавить обработчики.
Можно, только зачем? Один из весомых бонусов подхода из статьи — код строго соответствует бизнес логике, вы читаете код и понимаете правило. Когда же вы видите класс, у которого есть оба поля, и они оба необязательные, а консистентность обеспечивается наличием двух конструкторов — новый разработчик добавит конструктор по умолчанию, и ваша защита на этом кончилась.
Причем это не будет выглядеть ошибкой или ломающим изменением — структура объекта предполагает одновременное отсутствие обоих полей, конструктор по умолчанию лишь позволяет это состояние создать.
И напротив, когда дизайн как в статье, на Discriminated Unions
(они же типы-суммы и размеченные объединения), он говорит разработчику, не знакомому с кодом, что одно из полей обязательное. И добавление нового кейса без обязательных полей уже в явном виде говорит, что мы меняем бизнес логику
Другое дело, что такой подход в ситуации с независимыми полями просто не нужен. А вот если они почти независимы, но из 10 полей должно быть заполнено хотя бы одно — привет комбинаторный взрыв и 1023 конструктора у типа-суммы :-)
Кейс редкий, но допустим.
Во-первых, коль скоро мы говорим про F#, тут можно без каких либо проблем применять ООП подход: вам доступны и традиционные мутабельные ссылочные типы, и Nullable
, и куда более удобный Option<T>
(который, кстати, тоже тип-сумма). И вы можете задизайнить в привычной манере.
Во-вторых, если концепция плохо работает на каком-то радикальном случае, это не значит, что концепция плоха в принципе. Любой инструмент имеет смысл в рамках решения задачи, и если конкретная задача решается с его помощью плохо — всегда можно выбрать другой.
В-третьих, кейс с большим количеством полей можно вообще спроектировать принципиально по-другому, например, как массив объектов нашего типа-суммы (в котором 10 кейсов), и валидировать, что длина больше 0. А лучше вообще завернуть это в еще один тип-сумму
type MyInfo = | Invalid | Info of MyDU list
Короче говоря, если у вас нет миллионов опциональных полей, из которых только сотня обязательна, то такой дизайн поможет перетащить некоторые проблемы из рантайма в компайл тайм, а это огромный бонус.
Например, если у вас 5 опциональных полей, но некоторые связаны особой логикой (если заполнено это, то должно быть заполнно и то и это)
то разве не напишите вы тонну кода или фабрик? Не будт ли и у вас тут комбинаторный взрыв?
Ну и тесты, ими надо покрыть каждый кейс, а тут за вас «тесты» пишет и прогоняет компилятор.
Мне кажется в этой ситуации будет более уместным ОО-подход: полиморфный базовый тип, хранимый в списке, и набор конкретных реализаций контактов. В этом случае инвариантом будет наличие как минимум 2 элементов в списке в любой момент времени.
Проектирование типами: Как сделать некорректные состояния невыразимыми