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? Я говорю про обработчик нажатия на кнопку "положить в корзину". Как вы его будете делать?
Для оплаченной корзины этой кнопки вообще не должно существовать, поэтому и обрабатывать нечего.
Мне больше нравится с Union Type.
Есть TabType, который реализован с помощю Uniont Type-а.
Есть Tab у которого 3 состояния: DefaultTab, ClosedTab, OpendTab и соответствующие интерфейсы.
А вот переход из одного состояния в другое.
Для данного как раз важно. Мы рефакторим конкретный класс с условиями, после рефакторинга в нём условий не остаётся. Будут ли они на уровне клиентов этого класса или их вобще не будет в вопросе рефакторинга этого класса не важно.
И без карт/хешей можно избавиться от условий в частных случаях, когда тип задаётся статически, явно в коде. Если же тип вычисляется динамически, например по пользовательскому вводу, то тут без условий (или карты/хэш-таблицы,, или паттерн-матчинга, которые под капотом аналоги switch) не обойтись. Поведенческие паттерны лишь позволяют вынести эту логику в клиентские классы.
Простите, а "нормально" — это как?
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. Там реализованы два состояний разлогинин/залогинин и набор операций, которые можно запрограммировать, разный для каждого состояния.
Собираюсь в статью оформить, но не знаю, когда руки дойдут.
Шаблон проектирования «состояние» двадцать лет спустя