Как стать автором
Обновить

Комментарии 27

НЛО прилетело и опубликовало эту надпись здесь

Спасибо, исправил.

Спасибо за пример F# в реальной жизни :)
Полезная статья, один из больших бонусов F# именно в возможности сделать ненужные состояния невозможными, и проверить это на этапе компиляции.
Спасибо!

Не проще ли обработать это в конструкторе класса?

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

Так можно просто конструкторы сделать типа contactWithEmail и contactWithPostal — и тогда физически не получится создать Contact с обоими заполненными полями.

Всё равно тогда придётся учитывать случай с некорректным состоянием, по крайней мере, при использовании сопоставления с образцом. Объявление Contact будет проще, но использование сложнее.


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


  1. Приходит изменение: теперь у всех конатктов должен быть телефон и хотя бы один адрес для писем. Везде добавляется обработка телефона. При этом "невозможный" обработчик, где есть телефон, но нет ни электронной, ни обычной почты, остаётся.
  2. Новое изменение — теперь контакт с одним только телефоном возможен. Теперь "невозможные" обработчики могут отработать и их надо искать и обновлять.

При явном выделении состояний такой проблемы не возникнет, никаких "невозможных" обработчиков не будет, просто добавится новое состояние PhoneOnly и компилятор подскажет, где надо добавить обработчики.

Можно, только зачем? Один из весомых бонусов подхода из статьи — код строго соответствует бизнес логике, вы читаете код и понимаете правило. Когда же вы видите класс, у которого есть оба поля, и они оба необязательные, а консистентность обеспечивается наличием двух конструкторов — новый разработчик добавит конструктор по умолчанию, и ваша защита на этом кончилась.
Причем это не будет выглядеть ошибкой или ломающим изменением — структура объекта предполагает одновременное отсутствие обоих полей, конструктор по умолчанию лишь позволяет это состояние создать.


И напротив, когда дизайн как в статье, на Discriminated Unions (они же типы-суммы и размеченные объединения), он говорит разработчику, не знакомому с кодом, что одно из полей обязательное. И добавление нового кейса без обязательных полей уже в явном виде говорит, что мы меняем бизнес логику

Делаем пустой конструктор по умолчанию приватным, тем самым явно указывая на недопустимость этого действия.
Злонамеренные действия возможны при любом дизайне — это обычно костылём называют.
А аргументы, почему то, что я предложил — плохо?
НЛО прилетело и опубликовало эту надпись здесь
Почему-то у меня чувство, что при усложнении модели сложность такого кода будет расти экспоненциально.
Не будет на самом деле. Если у меня скажем усложниться модель EmailContactInfo, то усложниться только этот тип и код, который работает с деталями этого типа. Типы, которые содержат этот тип, будут работать как и прежде.
Если в ООП-подходе у класса ContactInfo N опциональных полей, то при использовании приведенного в статье подхода у типа ContactInfo будет до 2^N конструкторов.

Другое дело, что такой подход в ситуации с независимыми полями просто не нужен. А вот если они почти независимы, но из 10 полей должно быть заполнено хотя бы одно — привет комбинаторный взрыв и 1023 конструктора у типа-суммы :-)

Кейс редкий, но допустим.
Во-первых, коль скоро мы говорим про F#, тут можно без каких либо проблем применять ООП подход: вам доступны и традиционные мутабельные ссылочные типы, и Nullable, и куда более удобный Option<T> (который, кстати, тоже тип-сумма). И вы можете задизайнить в привычной манере.
Во-вторых, если концепция плохо работает на каком-то радикальном случае, это не значит, что концепция плоха в принципе. Любой инструмент имеет смысл в рамках решения задачи, и если конкретная задача решается с его помощью плохо — всегда можно выбрать другой.
В-третьих, кейс с большим количеством полей можно вообще спроектировать принципиально по-другому, например, как массив объектов нашего типа-суммы (в котором 10 кейсов), и валидировать, что длина больше 0. А лучше вообще завернуть это в еще один тип-сумму
type MyInfo = | Invalid | Info of MyDU list


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

Кроме комментария kagetoki, который безусловно логично оспаривает ваш кейс, хотелось бы понять как вы с большим набором полей будете поддерживать логику логических состояний (да еще и тесты писать)?
Например, если у вас 5 опциональных полей, но некоторые связаны особой логикой (если заполнено это, то должно быть заполнно и то и это)
то разве не напишите вы тонну кода или фабрик? Не будт ли и у вас тут комбинаторный взрыв?
Ну и тесты, ими надо покрыть каждый кейс, а тут за вас «тесты» пишет и прогоняет компилятор.
Выразительные возможности языка в рантайме, как правило, сильно больше выразительных возможностей языка в компайл тайме.

Очень зависит от языка и развитости его системы типов.

А как насчёт комбинаторного взрыва в случае, скажем, если из 5 доступных вариантов контактов (с возможностью расширения в будущем) необходимо заполнить как минимум 2?
Мне кажется в этой ситуации будет более уместным ОО-подход: полиморфный базовый тип, хранимый в списке, и набор конкретных реализаций контактов. В этом случае инвариантом будет наличие как минимум 2 элементов в списке в любой момент времени.
Господа минусующие, а аргументы можно?
Истина рождается в дискуссии.
НЛО прилетело и опубликовало эту надпись здесь
А как подобное сделать в F#? Вот есть, допустим, такая логика верификации пользователя, два-из-многих. Как описать систему типов для F# для её представления?
НЛО прилетело и опубликовало эту надпись здесь

В F# же есть ООП, совместимое со всем прочим дотнетом.

Извините, скорее всего я вас не так понял. Вы имели в виду "система типов F# слишком простая, чтобы решить это как-то помимо ООП"?

НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории