Государственный паттерн нарушает принцип подстановки Лискова?

17

Это изображение взято из применения доменного дизайна и шаблонов: с примерами в C # и .NET

введите описание изображения здесь

Это диаграмма классов для шаблона состояний, в котором в SalesOrderтечение срока службы может быть несколько состояний. Между разными состояниями разрешены только определенные переходы.

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

Разве это не нарушает принцип подстановки Лискова, поскольку подкласс не должен изменять поведение родителя? Исправляет ли это изменение абстрактного класса в интерфейс?
Как это можно исправить?

Songo
источник
Изменить OrderState на конкретный класс, который по умолчанию генерирует исключения NotSupported во всех своих методах?
Джеймс
Я не читал эту книгу, но мне кажется странным, что OrderState содержит Cancel (), а затем есть состояние Cancelled, то есть другой подтип.
Euphoric
@ Эйфорично, почему это странно? вызов cancel()меняет состояние на отмененное.
Сонго
Как? Как вы можете вызвать Cancel в OrderState, когда предполагается изменение экземпляра состояния в SalesOrder. Я думаю, что вся модель не так. Я хотел бы видеть полную реализацию.
Euphoric

Ответы:

3

Это конкретная реализация, да. Если вы сделаете состояния конкретными классами, а не абстрактными разработчиками, тогда вы избежите этого.

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

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

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

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

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

Джимми Хоффа
источник
По сути, большая проблема в том, что объектная ориентация хороша для добавления новых классов, но затрудняет добавление новых методов. И в случае состояний, как вы сказали, маловероятно, что вам потребуется очень часто расширять код новыми состояниями.
hugomg
3
@ Джимми Хоффа: Извините, но что вы имеете в виду именно: «Если вы сделаете штаты конкретными классами, а не абстрактными разработчиками, тогда вы избежите этого». ?
Сонго
@Jimmy: я пытался реализовать состояние без шаблона состояния, когда каждый компонент знал только о своем собственном состоянии. В результате большие изменения в потоке значительно усложнились для внедрения и обслуживания. Тем не менее, я думаю, что использование конечного автомата (в идеале чья-то библиотека, чтобы заставить его обрабатываться как черный ящик), а не шаблон проектирования состояний (таким образом, состояния - это просто элементы в перечислении, а не полные классы, а переходы - просто тупые ребра) дает много преимуществ паттерна состояния, будучи враждебным попыткам разработчиков злоупотреблять им.
Брайан
8

подкласс не должен изменять поведение родителя?

Это распространенное неправильное толкование LSP. Подкласс может изменить поведение родителя, если он остается верным для родительского типа.

Есть хорошее длинное объяснение Википедии , предлагающее вещи, которые нарушат LSP:

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

  • Предпосылки не могут быть усилены в подтипе.
  • Постусловия не могут быть ослаблены в подтипе.
  • Инварианты супертипа должны быть сохранены в подтипе.
  • Ограничение истории («правило истории»). Объекты считаются изменяемыми только через их методы (инкапсуляция). Поскольку подтипы могут вводить методы, которых нет в супертипе, введение этих методов может разрешить изменения состояния в подтипе, которые не допускаются в супертипе. Ограничение истории запрещает это. Это был новый элемент, представленный Лисковым и Крылом. Нарушение этого ограничения может быть проиллюстрировано определением MutablePoint как подтипа ImmutablePoint. Это нарушение ограничения истории, поскольку в истории неизменяемой точки состояние всегда остается одним и тем же после создания, поэтому оно не может включать историю MutablePoint в целом. Поля, добавленные к подтипу, могут, однако, быть безопасно изменены, потому что они не наблюдаются с помощью методов супертипа.

Лично мне проще запомнить это: если я смотрю на параметр в методе с типом A, может ли кто-нибудь передать подтип B вызвать какие-либо сюрпризы? Если они будут, то есть нарушение LSP.

Является ли исключение сюрпризом? На самом деле, нет. Это может произойти в любое время, независимо от того, вызываю ли я метод Ship в OrderState или Granted или Shipped. Поэтому я должен учитывать это, и это не является нарушением LSP.

Тем не менее , я думаю, что есть лучшие способы справиться с этой ситуацией. Если бы я писал это на C #, я бы использовал интерфейсы и проверил бы реализацию интерфейса перед вызовом метода. Например, если текущий OrderState не реализует IShippable, не вызывайте метод Ship для него.

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

Итак, в двух словах, это плохо продуманный пример Государственного паттерна и не очень хороший способ обработки состояния заказа. Но это, возможно, не нарушает LSP. И государственная структура, сама по себе, конечно , нет.

прецизионный самописец
источник
Мне кажется, что вы путаете LSP с Принципом наименьшего удивления / удивления, я считаю, что LSP имеет более четкие границы, когда это их нарушает, хотя вы применяете более субъективную POLA, которая основана на субъективных ожиданиях ; то есть каждый человек ожидает чего-то другого, чем следующий. LSP основывается на договорных и четко определенных гарантиях, данных поставщиком, а не на субъективно оцененных ожиданиях, предполагаемых потребителем.
Джимми Хоффа
@JimmyHoffa: Вы правы, и именно поэтому я изложил точное значение, прежде чем изложить более простой способ запоминания LSP. Однако, если у меня возникнет вопрос, что означает «сюрприз», тогда я вернусь и проверим точные правила LSP (вы действительно можете вспомнить их по желанию?). Исключения, заменяющие функциональность (или наоборот), являются сложными, поскольку они не нарушают какой-либо конкретный пункт выше, что, вероятно, является подсказкой того, что с ними следует обращаться совершенно по-другому.
фунтовые
1
Исключением является ожидаемое условие, определенное в контракте, на мой взгляд, подумайте о NetworkSocket, у него может быть ожидаемое исключение при отправке, если оно не открыто, если ваша реализация не выдает это исключение для закрытого сокета, который вы нарушаете LSP Если в контракте указано обратное, а ваш подтип выдает исключение, значит, вы нарушаете LSP. Это более или менее то, как я классифицирую исключения в LSP. Что касается запоминания правил; Я могу ошибаться в этом и не стесняйтесь говорить мне об этом, но мое понимание LSP против POLA определено в последнем предложении моего комментария выше.
Джимми Хоффа
1
@JimmyHoffa: Я никогда не думал, что POLA и LSP будут иметь хоть какое-то дистанционное соединение (я думаю о POLA в терминах пользовательского интерфейса), но теперь, когда вы на это указали, они вроде как. Я не уверен, что они находятся в конфликте, или что один является более субъективным, чем другой. Разве LSP не является ранним описанием POLA в терминах разработки программного обеспечения? Точно так же, как я расстраиваюсь, когда Ctrl-C не копирует (POLA), я также расстраиваюсь, когда ImmutablePoint является изменяемым, потому что на самом деле это подкласс MutablePoint (LSP).
фунтовые
1
Я бы рассмотрел CircleWithFixedCenterButMutableRadiusв качестве вероятного нарушения LSP, если в потребительском договоре ImmutablePointуказано, что если X.equals(Y)потребители могут свободно заменить X на Y, и наоборот, и, следовательно, должны воздерживаться от использования класса таким образом, чтобы такая замена вызывала проблемы. Тип может быть в состоянии законно определить так equals, что все экземпляры будут сравниваться как отдельные, но такое поведение, вероятно, ограничит его полезность.
суперкат
2

(Это написано с точки зрения C #, поэтому нет проверенных исключений.)

Согласно статье Википедии о LSP , одним из условий LSP является:

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

Как вы должны это понимать? Что именно являются «исключениями, создаваемыми методами супертипа», когда методы супертипа являются абстрактными? Я думаю, что это исключения, которые задокументированы как исключения, которые могут генерироваться методами супертипа.

Это означает, что если OrderState.Ship()задокументировано что-то вроде «throws, InvalidOperationExceptionесли эта операция не поддерживается текущим состоянием», то я думаю, что этот дизайн не нарушает LSP. С другой стороны, если методы супертипа не документированы таким образом, LSP нарушается.

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

svick
источник
На самом деле, именно этот момент заставил меня задуматься. Если подклассы генерируют исключения, не определенные в родительском объекте, это должно быть нарушением LSP.
Сонго
Я думаю, что на языке, где исключения не проверяются, выбрасывание их не является нарушением LSP. Это просто концепция самого языка, что в любом месте в любое время может быть выброшено любое исключение. Поэтому любой код клиента должен быть к этому готов.
Запад