Pull to refresh

Comments 26

Таким образом мы гарантируем отсутствие InvalidOperationException в нашем коде на этапе компиляции.

Как эта идея уживается с default: throw new InvalidOperationException();?


Кстати, в чем именно вы видите нарушение принципа подстановки Лисков?

Как эта идея уживается с default: throw new InvalidOperationException();?

Возможно, получится написать анализтор на Roslyn для того чтобы заставить в pattern matching обработать всех наследников. Тогда от этого InvalidOperationException в этом месте можно будет отказаться. Я думаю, что лучше одно исключение в pattern matching, чем неизвестно сколько в реализациях состояний.

Кстати, в чем именно вы видите нарушение принципа подстановки Лисков?

Есть ICartState { Add, Remove, Pay}. Мы не можем заменить ActiveCartState: ICartState на PaidCartState: ICartState, потому что оплаченное состояние выбросит исключение. Основная идея избавиться именно от этих InvalidOperationException.

Каким образом исключение мешает заменить ActiveCartState на PaidCartState? Исключение при попытке изменения оплаченной корзины — это часть бизнес-логики. Программа, которая выдает ошибку при попытке выполнения некорретной операции — работает корректно. Принцип подстановки тут не нарушается.


Основная идея избавиться именно от этих InvalidOperationException.

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

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

Добавляем шаблон «Форма не реализована» и вставляем в default и исключения не будет.

Программа, которая выдает ошибку при попытке выполнения некорретной операции — работает корректно.

Если для вас это корректное поведение и все устраивает, то вам и не нужно структурировать состояния таким образом.

Предлагаете перейти от исключений к особым возвращаемым значениям? Хорошо, вот вам встречное предложение. Делаем вот такой интерфейс:


        public interface ICartState
        {
            bool TryAdd(Product product);
            bool TryRemove(Product product);
            bool TryPay(decimal total);
            bool TryClear()
        }

Все! Исключения больше не нужны, все методы определены для всех состояний.


PS какие нафиг шаблон «Форма не реализована» и default? Я говорю про обработчик нажатия на кнопку "положить в корзину". Как вы его будете делать?

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

Это возможно только в локальном приложении — и то не так просто связать написанное в обсуждаемом посте с видимостью кнопки.


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

Статья на хорошую тему. Я думаю одна из важных моментов при проектировании предметной области. («Make illegal states unrepresentable»)

Мне больше нравится с Union Type.

Есть TabType, который реализован с помощю Uniont Type-а.
Есть Tab у которого 3 состояния: DefaultTab, ClosedTab, OpendTab и соответствующие интерфейсы.

А вот переход из одного состояния в другое.

Мне интересно, почему по ссылке на рефакторин гуру статья называется «заменяйте условные операторы полиморфизмом» предлагают использовать для этого паттерн стратегию, хотя он для этого не предназначен. Паттерн состояние — да, а стратегия — нет. И в приведенном там примере в итоге так и не избавились от условных операторов, так как все равно там придется их применить в месте где будем решать какой из классов стратегии использовать.
Стратегия переносит ответственность за выбор алгоритма на клиента, а за исполнение на стратегии. В некоторых случаях она реально позволяет избавиться от условных операторов или карт полностью, а в общем выносит их из целевого класса.
Она то переносит, но принимать решение все равно придется и для данного вопроса это не важно где, важно что мы не избавились от условных операторов. Как принимать решение о параметризации контекста той или иной стартегией вообще выходит за рамки этого паттерна. Ну ок, единственный способ, который я в данный момент вижу — это сделать хэш таблицу в которой значения — это стратегии, тогда правда можно. Но опять таки ж это не заслуга паттерна стратегия, можно в качестве значения хэш таблицы задать и функцию в конце-концов, так что.

Для данного как раз важно. Мы рефакторим конкретный класс с условиями, после рефакторинга в нём условий не остаётся. Будут ли они на уровне клиентов этого класса или их вобще не будет в вопросе рефакторинга этого класса не важно.


И без карт/хешей можно избавиться от условий в частных случаях, когда тип задаётся статически, явно в коде. Если же тип вычисляется динамически, например по пользовательскому вводу, то тут без условий (или карты/хэш-таблицы,, или паттерн-матчинга, которые под капотом аналоги switch) не обойтись. Поведенческие паттерны лишь позволяют вынести эту логику в клиентские классы.

И чего только люди ни придумают, лишь бы конечный автомат нормально не программировать.

Простите, а "нормально" — это как?

Нормально — выделить ответственность за реализацию собственно конечного автомата в отдельный класс.
А что нужно дальше? Уже на этом этапе никаким паттерном State даже не пахнет.
State от банды четырех — это конечный автомат, размазанный по множеству классов.
Нарушение SRP с особым цинизмом, кошмар для сопровождения. К счастью, в реализации он тоже труден и поэтому встречается редко.

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

Я обычно делаю в лоб: автомат реализую как граф, где состояния — вершины, а ребра — переходы (параметризованные событиями, по которым осуществляется переход).
Очень удобно — можно создать любой автомат, не меняя кода его реализации, просто указав состояния и переходы между ними.
Полученный автомат можно тестировать отдельно от остального приложения, проверяя реакции на события.
… и как вы храните переходы?
Переходы храняться как ребра графа — я же уже написал.
Как хранить графы вообще — немного за пределами темы данной статьи, не так ли?
Почему вы так скрываете свою реализацию?
Конечный автомат и состояние дополняют друг друга. Автомат обеспечивает изменение состояния, а состояние — изменение поведения.

Кроме принципа подстановки Лисков есть еще принцип открытости/закрытости. Предлагавшийся выше вариант с public interface ICartState куда лучше ему соответствует, ИМХО. В предлагаемом Вами варианте добавление новых состояний при изменении требований будет вызывать боль из-за разрастающихся switch-ей и необходимости изменять множество мест сразу — хотя именно этого хотелось бы избежать.


Навскидку: от бизнеса может придти требование — "а давайте, пользователь может в уже оплаченной корзине добавлять или убирать бесплатные опции — доставку, например, или подарок...". И вся стройная система состояний ломается, т.к. в PaidCartState внезапно надо добавить методы Add(Product product) и Remove(Product product) с дополнительными валидациями, а точно ли можно этот product в данном состоянии Add или Remove. Так что SRP размывается тоже.

боль из-за разрастающихся switch-ей и необходимости изменять множество мест сразу — хотя именно этого хотелось бы избежать.

Вы правы по поводу switch'ей. Поэтому я в конце добавил, что использование полиморфизма предпочтительней для control flow. Pattern matching хорошо подходит там где между состояниями нет ничего общего. По идее такие switch'и должны встречаться только в передаче управления в слой представления. Если состояния абсолютно разные, то вам действительно необходимо отредактировать каждый switch, потому что общего поведения нет и в каждом контексте требуется своя обработка ситуации.

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

Вы реализуете в PaidCartState интерфейс IAddableCartState и продолжите работать по интерфейсной ссылке, используя полиморфизм.
Полноценно решить такие проблемы можно в языках с зависимыми типами, которые позволяют описать зависимость интерфейса от значения состояния. Но получается довольно сложно.
Я набросал похожий пример на Idris и Scala. Там реализованы два состояний разлогинин/залогинин и набор операций, которые можно запрограммировать, разный для каждого состояния.
Собираюсь в статью оформить, но не знаю, когда руки дойдут.
Sign up to leave a comment.

Articles