Я пытаюсь понять твердые принципы ООП и пришел к выводу, что у LSP и OCP есть некоторые сходства (если не сказать больше).
принцип открытого / закрытого состояния гласит, что «программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации».
Проще говоря, LSP гласит, что любой экземпляр Foo
можно заменить любым экземпляром, Bar
который получен из Foo
программы, и программа будет работать точно так же.
Я не профессиональный программист ООП, но мне кажется, что LSP возможен только в том случае Bar
, если производный от него Foo
ничего не меняет, а только расширяет. Это означает, что в конкретной программе LSP имеет значение «истина» только тогда, когда OCP имеет значение «истина», а OCP - «истина» только тогда, когда «LSP» имеет значение «истина». Это означает, что они равны.
Поправьте меня если я ошибаюсь. Я действительно хочу понять эти идеи. Большое спасибо за ответ.
Square
отRectangle
не нарушает LSP. (Но это, вероятно, все еще плохой дизайн в неизменном случае, поскольку у вас могут быть квадратыRectangle
s,Square
которые не соответствуют математике)Ответы:
Черт возьми, есть некоторые странные заблуждения относительно того, что такое OCP и LSP, а некоторые из-за несоответствия некоторых терминов и запутанных примеров. Оба принципа - это «одно и то же», если вы реализуете их одинаково Шаблоны обычно так или иначе следуют принципам, за редким исключением.
Различия будут объяснены ниже, но сначала давайте взглянем на сами принципы:
Открытый Закрытый Принцип (OCP)
По словам дяди Боба :
Обратите внимание, что в этом случае слово extension не обязательно означает, что вы должны создавать подкласс реального класса, которому необходимо новое поведение. Видите, как я упомянул сначала несоответствие терминологии? Ключевое слово
extend
означает только создание подклассов в Java, но принципы старше, чем в Java.Оригинал пришел от Бертран Мейер в 1988 году:
Здесь гораздо понятнее, что этот принцип применяется к программным объектам . Плохой пример - переопределение программной сущности, когда вы полностью модифицируете код, вместо того, чтобы предоставить какую-то точку расширения. Поведение самого объекта программного обеспечения должно быть расширяемым, и хорошим примером этого является реализация паттерна Strategy (потому что это самый простой способ показать связку GoF-паттернов IMHO):
В приведенном выше примере
Context
будет заблокирована для дальнейших модификаций. Большинство программистов, вероятно, хотели бы разделить класс на подклассы, чтобы расширить его, но здесь мы этого не делаем, поскольку предполагается, что его поведение можно изменить с помощью всего, что реализуетIBehavior
интерфейс.Т.е. класс контекста закрыт для модификации, но открыт для расширения . Это фактически следует другому основному принципу, потому что мы помещаем поведение с составом объекта вместо наследования:
Я позволю читателю прочитать этот принцип, поскольку он выходит за рамки этого вопроса. Для продолжения примера, скажем, у нас есть следующие реализации интерфейса IBehavior:
Используя этот шаблон, мы можем изменить поведение контекста во время выполнения, используя
setBehavior
метод как точку расширения.Поэтому, когда вы хотите расширить «закрытый» класс контекста, сделайте это, создав подклассы его «открытой» совместной зависимости. Это явно не то же самое, что подклассы самого контекста, но это OCP. LSP также не упоминает об этом.
Расширение с помощью Mixins вместо наследования
Есть и другие способы сделать OCP кроме подклассов. Один из способов - оставить ваши классы открытыми для расширения с помощью миксинов . Это полезно, например, в языках, основанных на прототипах, а не на классах. Идея состоит в том, чтобы дополнить динамический объект большим количеством методов или атрибутов по мере необходимости, другими словами, объектов, которые смешиваются или «смешиваются» с другими объектами.
Вот пример javascript миксина, который отображает простой HTML-шаблон для якорей:
Идея состоит в том, чтобы динамически расширять объекты, и преимущество в том, что объекты могут совместно использовать методы, даже если они находятся в совершенно разных областях. В приведенном выше случае вы можете легко создавать другие виды html-якорей, расширяя вашу конкретную реализацию с помощью
LinkMixin
.С точки зрения OCP, "mixins" являются расширениями. В приведенном выше примере
YoutubeLink
это наша программная сущность, которая закрыта для модификации, но открыта для расширений за счет использования mixins. Иерархия объектов выровнена, что делает невозможным проверку типов. Однако это не так уж и плохо, и я объясню в дальнейшем, что проверка типов, как правило, является плохой идеей и нарушает ее с помощью полиморфизма.Обратите внимание, что с помощью этого метода можно сделать множественное наследование, так как большинство
extend
реализаций могут смешивать несколько объектов:Единственное, что вам нужно иметь в виду, это не конфликтовать с именами, т. Е. Миксины определяют одно и то же имя некоторых атрибутов или методов, поскольку они будут переопределены. По моему скромному опыту, это не проблема, и если это произойдет, это признак неправильного дизайна.
Принцип замещения Лискова (LSP)
Дядя Боб определяет это просто:
Этот принцип старый, на самом деле определение дяди Боба не дифференцирует принципы, поскольку делает LSP по-прежнему тесно связанным с OCP, поскольку в приведенном выше примере стратегии используется тот же супертип (
IBehavior
). Итак, давайте посмотрим на его первоначальное определение Барбары Лисков и посмотрим, сможем ли мы найти что-то еще об этом принципе, которое выглядит как математическая теорема:Давай пожмем на это какое-то время, обратите внимание, поскольку в нем вообще не упоминаются занятия. В JavaScript вы можете следовать LSP, хотя он явно не основан на классах. Если ваша программа имеет список хотя бы из пары объектов JavaScript, которые:
... тогда объекты рассматриваются как имеющие один и тот же "тип", и это не имеет значения для программы. Это по сути полиморфизм . В общем смысле; вам не нужно знать фактический подтип, если вы используете его интерфейс. OCP ничего не говорит об этом прямо. Это также фактически указывает на ошибку проектирования, которую делают большинство начинающих программистов:
Всякий раз, когда вы чувствуете желание проверить подтип объекта, вы, скорее всего, делаете это НЕПРАВИЛЬНО.
Хорошо, так что это может быть не всегда неправильно, но если у вас есть желание сделать какую-то проверку типов с помощью
instanceof
или перечислений, вы можете сделать программу немного более сложной для себя, чем это необходимо. Но это не всегда так; быстрые и грязные взломы, чтобы заставить вещи работать, - нормальная уступка, на мой взгляд, если решение достаточно мало, и если вы практикуете беспощадный рефакторинг , оно может улучшиться, как только изменения потребуют этого.Есть несколько способов обойти эту «ошибку проектирования», в зависимости от актуальной проблемы:
Обе они являются общими «ошибками» дизайна кода. Существует несколько различных способов рефакторинга , таких как метод подтягивания или рефакторинг для шаблона, такого как шаблон Visitor .
Мне действительно очень нравится шаблон Visitor, так как он может позаботиться о больших спагетти if-операторов, и его проще реализовать, чем то, что вы думаете о существующем коде. Скажем, у нас есть следующий контекст:
Результаты оператора if могут быть переведены в их собственных посетителей, поскольку каждый из них зависит от того или иного решения и какого-либо кода для запуска. Мы можем извлечь их так:
На этом этапе, если программист не знает о шаблоне Visitor, он вместо этого реализует класс Context, чтобы проверить, имеет ли он какой-то определенный тип. Поскольку классы Visitor имеют логический
canDo
метод, разработчик может использовать этот метод, чтобы определить, является ли объект правильным для выполнения работы. Класс контекста может использовать всех посетителей (и добавлять новых) следующим образом:Оба шаблона следуют OCP и LSP, однако оба они указывают на разные вещи о них. Так как же выглядит код, если он нарушает один из принципов?
Нарушение одного принципа, но следование другому
Есть способы нарушить один из принципов, но все же нужно придерживаться другого. Приведенные ниже примеры кажутся надуманными, не зря, но на самом деле я видел, как они всплывают в рабочем коде (и даже хуже):
Следует OCP, но не LSP
Допустим, у нас есть данный код:
Этот кусок кода следует принципу открытого-закрытого. Если мы вызываем метод контекста
GetPersons
, мы получим группу людей со своими реализациями. Это означает, что IPerson закрыта для модификации, но открыта для расширения. Однако, когда мы используем его, все становится темным:Вы должны сделать проверку типов и преобразование типов! Помните, как я упоминал выше, что проверка типов - это плохо ? о нет! Но не бойтесь, как уже упоминалось выше, либо проведите рефакторинг подтягивания, либо внедрите шаблон Visitor. В этом случае мы можем просто выполнить рефакторинг после добавления общего метода:
Преимущество теперь в том, что вам не нужно больше знать точный тип после LSP:
Следует за LSP, но не OCP
Давайте посмотрим на некоторый код, который следует за LSP, но не OCP, он немного надуманный, но потерпите меня на этом, это очень тонкая ошибка:
Код выполняет LSP, потому что контекст может использовать LiskovBase, не зная фактического типа. Вы могли бы подумать, что этот код также следует за OCP, но посмотрите внимательно, действительно ли класс закрыт ? Что если
doStuff
метод сделал больше, чем просто распечатал строку?Ответ, если он следует за OCP, прост: НЕТ , это не потому, что в этом объектном дизайне мы должны полностью переопределить код чем-то другим. Это открывает червяк для вырезания и вставки, поскольку вам нужно скопировать код из базового класса, чтобы все заработало.
doStuff
Метод уверен , открыт для расширения, но он не был полностью закрыт для модификации.Мы можем применить шаблон шаблон шаблона к этому. Шаблонный шаблонный шаблон настолько распространен в фреймворках, что вы могли использовать его, не зная его (например, компоненты Java-свинга, формы и компоненты c # и т. Д.). Вот один из способов закрыть
doStuff
метод для модификации и убедиться, что он остается закрытым, пометив егоfinal
ключевым словом java . Это ключевое слово не позволяет кому-либо в дальнейшем создавать подклассы класса (в C # вы можете использоватьsealed
то же самое).Этот пример следует за OCP и кажется глупым, что это так, но представьте, что это увеличено с большим количеством кода для обработки. Я продолжаю видеть код, развернутый в производстве, где подклассы полностью переопределяют все, а переопределенный код в большинстве случаев вырезан между вставками. Это работает, но, как и при любом дублировании кода, это также настройка для техобслуживающих кошмаров.
Заключение
Я надеюсь, что все это проясняет некоторые вопросы, касающиеся OCP и LSP и различий / сходств между ними. Их легко отклонить как одно и то же, но приведенные выше примеры должны показать, что это не так.
Обратите внимание, что, собрав сверху пример кода:
OCP - это блокировка рабочего кода, но он все равно остается открытым с некоторыми точками расширения.
Это сделано для того, чтобы избежать дублирования кода путем инкапсуляции кода, который изменяется, как в примере шаблона Template Method. Это также позволяет быстро потерпеть неудачу, поскольку ломать изменения болезненно (то есть менять одно место, ломать его везде). Для поддержания концепции концепция инкапсуляции изменений - это хорошо, потому что изменения всегда происходят.
LSP позволяет пользователю обрабатывать различные объекты, которые реализуют супертип, без проверки того, что это за тип. Это по сути то, что такое полиморфизм .
Этот принцип предоставляет альтернативу для выполнения проверки типов и преобразования типов, которая может выйти из-под контроля по мере роста числа типов и может быть достигнута с помощью рефакторинга подтягивания или применения таких шаблонов, как Visitor.
источник
Это то, что вызывает много путаницы. Я предпочитаю рассматривать эти принципы несколько философски, потому что для них есть много разных примеров, а иногда конкретные примеры не отражают всей их сути.
Что OCP пытается исправить
Скажем, нам нужно добавить функциональность в данную программу. Самый простой способ сделать это, особенно для людей, которые были обучены процессуальному мышлению, - это добавить условие if, где это необходимо, или что-то в этом роде.
Проблемы с этим
Вы можете сделать это, добавив дополнительное поле ко всем книгам с именем «is_on_sale», а затем вы можете проверить это поле при печати цены любой книги, или, в качестве альтернативы , вы можете создать экземпляр книги в продаже из базы данных, используя другой тип, который печатает «(В ПРОДАЖЕ)» в ценовой категории (не идеальный дизайн, но он дает смысл домой).
Проблема с первым, процедурным решением, является дополнительным полем для каждой книги и дополнительной избыточной сложностью во многих случаях. Второе решение заставляет логику только там, где это действительно необходимо.
Теперь рассмотрим тот факт, что может быть много случаев, когда требуются разные данные и логика, и вы поймете, почему стоит помнить об OCP при разработке классов или реагировании на изменения требований.
Теперь вы должны понять основную идею: попробуйте поставить себя в ситуацию, когда новый код может быть реализован в виде полиморфных расширений, а не процедурных модификаций.
Но никогда не бойтесь анализировать контекст и смотреть, не перевешивают ли недостатки те или иные преимущества, потому что даже такой принцип, как OCP, может создать беспорядок в 20 классах из 20-строчной программы, если не соблюдать осторожность .
Что пытается исправить LSP
Мы все любим повторное использование кода. Следующее заболевание заключается в том, что многие программы не понимают его полностью, вплоть до того, что они слепо учитывают общие строки кода, создавая нечитаемые сложности и избыточную тесную связь между модулями, которые, кроме нескольких строк кода, не имеют ничего общего с концептуальной работой.
Самый большой пример этого - повторное использование интерфейса . Вы, вероятно, были свидетелями этого сами; класс реализует интерфейс не потому, что его логическая реализация (или расширение в случае конкретных базовых классов), а потому, что методы, которые он объявляет в этой точке, имеют правильные сигнатуры в той степени, в которой это его касается.
Но тогда вы сталкиваетесь с проблемой. Если классы реализуют интерфейсы только с учетом сигнатур методов, которые они объявляют, то вы обнаруживаете, что можете передавать экземпляры классов из одной концептуальной функциональности в места, которые требуют совершенно другой функциональности, которая зависит только от похожих сигнатур.
Это не так уж и ужасно, но это вызывает много путаницы, и у нас есть технология, позволяющая нам не допускать подобных ошибок. Нам нужно рассматривать интерфейсы как протокол API + . API очевиден в декларациях, а протокол очевиден в существующих применениях интерфейса. Если у нас есть 2 концептуальных протокола, которые используют один и тот же API, они должны быть представлены как 2 разных интерфейса. В противном случае мы попадаем в догматизм DRY и, по иронии судьбы, создаем только сложный код для поддержки.
Теперь вы должны быть в состоянии понять определение отлично. В LSP говорится: не наследуйте от базового класса и не реализуйте функциональность в тех подклассах, с которыми другие места, которые зависят от базового класса, не будут ладить.
источник
Из моего понимания:
OCP говорит: «Если вы добавите новую функциональность, создайте новый класс, расширяющий существующий, а не меняя его».
LSP говорит: «Если вы создаете новый класс, расширяющий существующий класс, убедитесь, что он полностью взаимозаменяем с его базой».
Поэтому я думаю, что они дополняют друг друга, но они не равны.
источник
Хотя верно то, что OCP и LSP связаны с модификацией, о типе изменений, о котором говорит OCP, - не тот, о котором говорит LSP.
Изменение в отношении OCP - это физическое действие разработчика, пишущего код в существующем классе.
LSP имеет дело с модификацией поведения, которую приносит производный класс по сравнению с его базовым классом, и изменением времени выполнения программы, которое может быть вызвано использованием подкласса вместо суперкласса.
Таким образом, хотя они могут выглядеть похожими на расстоянии OCP! = LSP. На самом деле, я думаю, что они могут быть единственными 2 твердыми принципами, которые не могут быть поняты друг с другом.
источник
Это не верно. LSP утверждает, что класс Bar не должен вводить поведение, которое не ожидается, когда код использует Foo, когда Bar является производным от Foo. Это не имеет ничего общего с потерей функциональности. Вы можете удалить функциональность, но только когда код, использующий Foo, не зависит от этой функциональности.
Но, в конце концов, этого обычно трудно достичь, потому что большую часть времени код, использующий Foo, зависит от всего его поведения. Так что удаление его нарушает LSP. Но упростить его до такой степени - это только часть LSP.
источник
Об объектах, которые могут нарушать
Чтобы понять разницу, вы должны понимать предметы обоих принципов. Это не какая-то абстрактная часть кода или ситуации, которая может нарушать какой-либо принцип. Это всегда какой-то конкретный компонент - функция, класс или модуль - который может нарушать OCP или LSP.
Кто может нарушать LSP
Можно проверить, не нарушен ли LSP, только когда есть интерфейс с некоторым контрактом и реализация этого интерфейса. Если реализация не соответствует интерфейсу или, вообще говоря, контракту, то LSP нарушается.
Простейший пример:
В контракте четко указано, что
addObject
следует добавить свой аргумент в контейнер. ИCustomContainer
явно нарушает этот контракт. Таким образомCustomContainer.addObject
функция нарушает LSP. Таким образом,CustomContainer
класс нарушает LSP. Наиболее важным последствием является то, чтоCustomContainer
не может быть переданоfillWithRandomNumbers()
.Container
не может быть заменено наCustomContainer
.Имейте в виду, очень важный момент. Не весь этот код нарушает LSP, а именно
CustomContainer.addObject
и вообщеCustomContainer
нарушает LSP. Когда вы заявляете, что LSP нарушается, вы всегда должны указывать две вещи:Вот и все. Просто договор и его реализация. Понижение в коде ничего не говорит о нарушении LSP.
Кто может нарушать OCP
Можно проверить, нарушается ли OCP, только когда существует ограниченный набор данных и компонент, который обрабатывает значения из этого набора данных. Если пределы набора данных могут меняться со временем и это требует изменения исходного кода компонента, то этот компонент нарушает OCP.
Звучит сложно. Давайте попробуем простой пример:
Набор данных - это набор поддерживаемых платформ.
PlatformDescriber
является компонентом, который обрабатывает значения из этого набора данных. Добавление новой платформы требует обновления исходного кодаPlatformDescriber
. Таким образом,PlatformDescriber
класс нарушает OCP.Другой пример:
«Набор данных» - это набор каналов, в которые следует добавить запись журнала.
Logger
является компонентом, отвечающим за добавление записей во все каналы. Добавление поддержки другого способа ведения журнала требует обновления исходного кодаLogger
. Таким образом,Logger
класс нарушает OCP.Обратите внимание, что в обоих примерах набор данных не является чем-то семантически фиксированным. Это может измениться со временем. Может появиться новая платформа. Новый канал регистрации может появиться. Если ваш компонент должен быть обновлен, когда это произойдет, он нарушает OCP.
Раздвигая пределы
Теперь сложная часть. Сравните приведенные выше примеры со следующими:
Вы можете думать, что
translateToRussian
нарушает OCP. Но на самом деле это не так.GregorianWeekDay
имеет определенный лимит ровно 7 дней в неделю с точными именами. И важно то, что эти пределы семантически не могут меняться со временем. В григорианской неделе всегда будет 7 дней. Всегда будет понедельник, вторник и т. Д. Этот набор данных семантически фиксирован. Не возможно, чтоtranslateToRussian
исходный код потребует изменений. Таким образом, OCP не нарушается.Теперь должно быть ясно, что исчерпывающее
switch
утверждение не всегда является признаком нарушения OCP.Различия
Теперь почувствуй разницу:
Эти условия полностью ортогональны.
Примеры
В @ ответ Spoike в Нарушая один принцип , но после другой стороны совершенно неправильно.
В первом примере
for
часть -loop явно нарушает OCP, потому что она не расширяема без изменений. Но нет никаких признаков нарушения LSP. И даже неясно,Context
позволяет ли контракт getPersons вернуть что-либо, кромеBoss
илиPeon
. Даже при условии, что контракт, который разрешаетIPerson
возвращать любой подкласс, не существует класса, который переопределяет это постусловие и нарушает его. Более того, если getPersons вернет экземпляр какого-то третьего класса,for
-loop выполнит свою работу без сбоев. Но этот факт не имеет ничего общего с LSP.Следующий. Во втором примере ни LSP, ни OCP не нарушаются. Опять же,
Context
деталь просто не имеет ничего общего с LSP - нет определенного контракта, нет подклассов, нет переопределенных переопределений. Это не тот,Context
кто должен подчиняться LSP, это неLiskovSub
должно нарушать контракт его базы. Что касается OCP, действительно ли класс закрыт? - Да, это. Никаких изменений не требуется, чтобы расширить его. Очевидно, что имя точки расширения указывает на то, что вы хотите, без ограничений . Пример не очень полезен в реальной жизни, но он явно не нарушает OCP.Давайте попробуем сделать несколько правильных примеров с истинным нарушением OCP или LSP.
Следуйте OCP, но не LSP
Здесь
HumanReadablePlatformSerializer
не требуется никаких изменений при добавлении новой платформы. Таким образом, следует OCP.Но контракт требует, чтобы
toJson
JSON возвращал правильно отформатированный файл. Класс не делает этого. Из-за этого он не может быть передан компоненту, который используетPlatformSerializer
для форматирования тела сетевого запроса. Таким образомHumanReadablePlatformSerializer
нарушает LSP.Следуйте LSP, но не OCP
Некоторые модификации предыдущего примера:
Сериализатор возвращает правильно отформатированную строку JSON. Таким образом, здесь нет нарушения LSP.
Но есть требование, что если платформа используется наиболее широко, то в JSON должна быть соответствующая индикация. В этом примере
HumanReadablePlatformSerializer.isMostPopular
функция OCP нарушается функцией, потому что когда-нибудь iOS станет самой популярной платформой. Формально это означает, что набор наиболее используемых платформ пока определен как «Android» иisMostPopular
неадекватно обрабатывает этот набор данных. Набор данных не является семантически фиксированным и может свободно изменяться со временем.HumanReadablePlatformSerializer
исходный код должен быть обновлен в случае изменения.Вы также можете заметить нарушение Единой Ответственности в этом примере. Я намеренно сделал так, чтобы иметь возможность продемонстрировать оба принципа на одном предмете. Для исправления SRP вы можете извлечь
isMostPopular
функцию из внешнего интерфейсаHelper
и добавить параметр вPlatformSerializer.toJson
. Но это другая история.источник
LSP и OCP - это не одно и то же.
LSP говорит о правильности программы в ее нынешнем виде . Если экземпляр подтипа нарушит корректность программы при замене на код для типов предков, то вы продемонстрировали нарушение LSP. Возможно, вам придется смоделировать тест, чтобы показать это, но вам не придется менять основную базу кода. Вы проверяете саму программу, чтобы увидеть, соответствует ли она LSP.
OCP говорит о правильности изменений в программном коде, дельта из одной исходной версии в другую. Поведение не должно быть изменено. Это должно быть только продлено. Классический пример - добавление полей. Все существующие поля продолжают работать как прежде. Новое поле просто добавляет функциональность. Однако удаление поля, как правило, является нарушением OCP. Здесь вы проверяете дельту версии программы, чтобы увидеть, соответствует ли она OCP.
Так что это ключевое различие между LSP и OCP. Первый проверяет только кодовую базу в том виде , в каком он есть , второй проверяет только дельту кодовой базы от одной версии к следующей . Как таковые, они не могут быть одним и тем же, они определяются как проверяющие разные вещи.
Я приведу более формальное доказательство: сказать «LSP подразумевает OCP» означало бы дельту (потому что OCP требует одно, а не в тривиальном случае), а LSP не требует. Так что это явно неверно. И наоборот, мы можем опровергнуть «OCP подразумевает LSP», просто сказав, что OCP - это утверждение о дельтах, поэтому в нем ничего не говорится о выражении над программой на месте. Это следует из того факта, что вы можете создать ЛЮБУЮ дельту, начиная с ЛЮБОЙ программы на месте. Они полностью независимы.
источник
Я бы посмотрел на это с точки зрения клиента. если Клиент использует функции интерфейса и внутренне эта функция была реализована классом А. Предположим, есть класс B, который расширяет класс A, тогда завтра, если я удалю класс A из этого интерфейса и добавлю класс B, тогда класс B должен также предоставить те же функции для клиента. Стандартным примером является класс Duck, который плавает, и если ToyDuck расширяет Duck, то он также должен плавать и не жаловаться, что не умеет плавать, в противном случае ToyDuck не должен был бы расширять класс Duck.
источник