Дискриминационные союзы конфликтуют с принципом открытого закрытия

Я не могу не сомневаться в том, что использование Дискриминационных Союзов в большой системе нарушает принцип Open/Close.

Я понимаю, что принцип Open/Close является объектно-ориентированным и НЕ функциональным. Однако у меня есть основания полагать, что существует тот же кодовый запах.

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

Таким образом, я по-прежнему считаю, что Дискриминационные Союзы имеют тот же кодовый запах, что и switch-statements.

Являются ли мои мысли точными?

Почему заявления переключателей нахмурились, но объявлены Дискриминационные союзы?

Разве мы не сталкиваемся с теми же проблемами обслуживания с использованием Дискриминационных Союзов, поскольку мы делаем switch-statements, поскольку кодовая база развивается или отступает?

Ответ 1

По-моему, принцип Open/Closed немного нечеткий - что означает "open for extension"?

Означает ли это расширение с новыми данными или расширение с новым поведением или оба?

Здесь цитата из Betrand Meyer (взята из Wikipedia):

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

И вот цитата из статьи Роберта Мартина :

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

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

В объектно-ориентированной парадигме (на основе поведения) я бы это интерпретировал как рекомендацию использовать интерфейсы (или абстрактные базовые классы). Тогда, если изменения требований, вы либо создаете новую реализацию существующего интерфейса, либо, если требуется новое поведение, создайте новый интерфейс, который расширяет оригинальный. (И BTW, операторы switch не OO - вы должны использовать полиморфизм!)

В функциональной парадигме эквивалент интерфейса с дизайнерской точки зрения является функцией. Так же, как вы передадите интерфейс объекту в OO-дизайне, вы передадите функцию в качестве параметра в другую функцию в FP-дизайне. Что еще, в FP, каждая сигнатура функции автоматически является "интерфейсом"! Реализация функции может быть изменена позже, пока его сигнатура функции не изменяется.

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

Расширение DU

Теперь в конкретном случае изменения требований к DU в F # вы можете расширить его, не затрагивая клиентов двумя способами.

  • Использовать композицию для создания нового типа данных из старого или
  • Скрыть случаи от клиентов и использовать активные шаблоны.

Скажите, что у вас простой DU, как это:

type NumberCategory = 
    | IsBig of int 
    | IsSmall of int 

И вы хотите добавить новый случай IsMedium.

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

type NumberCategoryV2 = 
    | IsBigOrSmall of NumberCategory 
    | IsMedium of int 

Для клиентов, которым нужен только оригинальный компонент NumberCategory, вы можете преобразовать новый тип в старый:

// convert from NumberCategoryV2 to NumberCategory
let toOriginal (catV2:NumberCategoryV2) =
    match catV2 with
    | IsBigOrSmall original -> original 
    | IsMedium i -> IsSmall i

Вы можете думать об этом как о каком-то явном повышении.

В качестве альтернативы вы можете скрыть случаи и выставить только активные шаблоны:

type NumberCategory = 
    private  // now private!
    | IsBig of int 
    | IsSmall of int 

let createNumberCategory i = 
    if i > 100 then IsBig i
    else IsSmall i

// active pattern used to extract data since type is private
let (|IsBig|IsSmall|) numberCat = 
    match numberCat with
    | IsBig i -> IsBig i 
    | IsSmall i -> IsSmall i 

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

type NumberCategory = 
    private
    | IsBig of int 
    | IsSmall of int 
    | IsMedium of int // new case added

let createNumberCategory i = 
    if i > 100 then IsBig i
    elif i > 10 then IsMedium i
    else IsSmall i

// active pattern used to extract data since type is private
let (|IsBig|IsSmall|) numberCat = 
    match numberCat with
    | IsBig i -> IsBig i 
    | IsSmall i -> IsSmall i 
    | IsMedium i -> IsSmall i // compatible with old definition

Какой подход лучше всего?

Ну, для кода, который я полностью контролирую, я бы не использовал ни одного - я бы просто внес изменения в DU и исправить ошибки компилятора!

Для кода, который отображается как API для клиентов, я не контролирую, я бы использовал активный шаблон.

Ответ 2

Объекты и дискриминационные объединения имеют ограничения, которые являются двойственными друг к другу:

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

Таким образом, DU определенно не подходят для моделирования каждой проблемы; но не являются традиционными проектами OO. Часто вы знаете, в каком "направлении" вам понадобятся будущие изменения, поэтому его легко выбрать (например, списки, безусловно, либо пустые, либо имеют голову и хвост, поэтому их моделирование через DU имеет смысл).

Иногда вы хотите расширить возможности в обоих направлениях (добавить новые "виды" объектов, а также добавить новые "операции" ) - это связано с проблемой выражения , и нет особо чистых решений в классическом программировании OO или классическом программировании FP (хотя возможны несколько барочных решений, см., например, комментарий Vesa Karvonen , который я транслитерировал в F # здесь).

Одна из причин, по которой DU могут быть более благоприятными, чем команды switch, заключается в том, что поддержка компилятора F # для проверки полноты и избыточности может быть более тщательной, чем, скажем, проверка компилятора С# операторов switch (например, если у меня есть match x with | A -> 'a' | B -> 'b' и Я добавляю новый случай DU C, тогда я получу предупреждение/ошибку, но при использовании enum в С# мне нужно иметь случай default, так что проверки времени компиляции не могут быть такими же сильными).

Ответ 3

Я не уверен, каков ваш подход к принципу Open-Close с OO, но я часто заканчиваю реализацию такого принципиального кода, используя функции более высокого порядка, другой подход, который я использую, - использовать интерфейсы. Я стараюсь избегать базовых классов.

Вы можете использовать тот же подход с DU, имея открытый для расширения случай, который имеет функтор как параметр поверх других полезных случаев, которые более жестко закодированы, например:

type Cases<T> =
| Case1 of string
| Case2 of int
| Case3 of IFoo
| OpenCase of (unit -> T)

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

Почему заявления переключателей нахмурились, но объявлены Дискриминационные союзы?

Вы можете сопоставить DU с сопоставлением с образцом, поэтому я попытаюсь уточнить:

Согласование шаблонов - это построение кода (например, switch), а DU - это тип конструкции (например, закрытая иерархия классов или структур или переименование).

Совпадение шаблонов с match в F # имеет больше возможностей, чем switch в С#.

Разве мы не сталкиваемся с теми же проблемами обслуживания с использованием Дискриминационных Союзов, поскольку мы делаем switch-statements, когда кодовая база развивается или отступает?

Дискриминационные союзы, используемые с сопоставлением с образцом, имеют больше свойств безопасности/исчерпания типов, чем обычный оператор switch, компилятор более полезен, поскольку он выдаст предупреждения для неполных совпадений, которые вы не получаете с операторами switch из С#.

У вас могут быть проблемы с обслуживанием с OO-кодом, который является Open-Close принципиальным, и я не чувствую, что DU связан с этим.

Ответ 4

Операторы Switch не противоречат принципу Open/Closed. Все зависит от того, где вы их разместили.

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

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

То же самое относится к сопоставлению шаблонов. Вы можете использовать его, чтобы решить, какую функцию перейти к функции более высокого порядка F (что было бы эквивалентом инъекции зависимости в OO). Это не означает, что сам F делает выбор или осознает, какая конкретная функция передается ему. Абстракция сохраняется.