Черт возьми, есть некоторые странные заблуждения относительно того, что такое OCP и LSP, а некоторые из-за несоответствия некоторых терминов и запутанных примеров. Оба принципа - это «одно и то же», если вы реализуете их одинаково Шаблоны обычно так или иначе следуют принципам, за редким исключением.
Различия будут объяснены ниже, но сначала давайте взглянем на сами принципы:
Открытый Закрытый Принцип (OCP)
По словам дяди Боба :
Вы должны иметь возможность расширять поведение классов, не изменяя его.
Обратите внимание, что в этом случае слово extension не обязательно означает, что вы должны создавать подкласс реального класса, которому необходимо новое поведение. Видите, как я упомянул сначала несоответствие терминологии? Ключевое слово extend
означает только создание подклассов в Java, но принципы старше, чем в Java.
Оригинал пришел от Бертран Мейер в 1988 году:
Программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации.
Здесь гораздо понятнее, что этот принцип применяется к программным объектам . Плохой пример - переопределение программной сущности, когда вы полностью модифицируете код, вместо того, чтобы предоставить какую-то точку расширения. Поведение самого объекта программного обеспечения должно быть расширяемым, и хорошим примером этого является реализация паттерна Strategy (потому что это самый простой способ показать связку GoF-паттернов IMHO):
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {
// Context is however open for extension through
// this private field
private IBehavior behavior;
// The context calls the behavior in this public
// method. If you want to change this you need
// to implement it in the IBehavior object
public void doStuff() {
if (this.behavior != null)
this.behavior.doStuff();
}
// You can dynamically set a new behavior at will
public void setBehavior(IBehavior behavior) {
this.behavior = behavior;
}
}
// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
public void doStuff();
}
В приведенном выше примере Context
будет заблокирована для дальнейших модификаций. Большинство программистов, вероятно, хотели бы разделить класс на подклассы, чтобы расширить его, но здесь мы этого не делаем, поскольку предполагается, что его поведение можно изменить с помощью всего, что реализует IBehavior
интерфейс.
Т.е. класс контекста закрыт для модификации, но открыт для расширения . Это фактически следует другому основному принципу, потому что мы помещаем поведение с составом объекта вместо наследования:
Msgstr "Фаворитировать" состав объекта "над" наследованием класса "." (Банда четырех 1995: 20)
Я позволю читателю прочитать этот принцип, поскольку он выходит за рамки этого вопроса. Для продолжения примера, скажем, у нас есть следующие реализации интерфейса IBehavior:
public class HelloWorldBehavior implements IBehavior {
public void doStuff() {
System.println("Hello world!");
}
}
public class GoodByeBehavior implements IBehavior {
public void doStuff() {
System.out.println("Good bye cruel world!");
}
}
Используя этот шаблон, мы можем изменить поведение контекста во время выполнения, используя setBehavior
метод как точку расширения.
// in your main method
Context c = new Context();
c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"
c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
Поэтому, когда вы хотите расширить «закрытый» класс контекста, сделайте это, создав подклассы его «открытой» совместной зависимости. Это явно не то же самое, что подклассы самого контекста, но это OCP. LSP также не упоминает об этом.
Расширение с помощью Mixins вместо наследования
Есть и другие способы сделать OCP кроме подклассов. Один из способов - оставить ваши классы открытыми для расширения с помощью миксинов . Это полезно, например, в языках, основанных на прототипах, а не на классах. Идея состоит в том, чтобы дополнить динамический объект большим количеством методов или атрибутов по мере необходимости, другими словами, объектов, которые смешиваются или «смешиваются» с другими объектами.
Вот пример javascript миксина, который отображает простой HTML-шаблон для якорей:
// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
render: function() {
return '<a href="' + this.link +'">'
+ this.content
+ '</a>;
}
}
// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
this.content = content;
this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
setLink: function(youtubeid) {
this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
}
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);
// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");
console.log(ytLink.render());
// will output:
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
Идея состоит в том, чтобы динамически расширять объекты, и преимущество в том, что объекты могут совместно использовать методы, даже если они находятся в совершенно разных областях. В приведенном выше случае вы можете легко создавать другие виды html-якорей, расширяя вашу конкретную реализацию с помощью LinkMixin
.
С точки зрения OCP, "mixins" являются расширениями. В приведенном выше примере YoutubeLink
это наша программная сущность, которая закрыта для модификации, но открыта для расширений за счет использования mixins. Иерархия объектов выровнена, что делает невозможным проверку типов. Однако это не так уж и плохо, и я объясню в дальнейшем, что проверка типов, как правило, является плохой идеей и нарушает ее с помощью полиморфизма.
Обратите внимание, что с помощью этого метода можно сделать множественное наследование, так как большинство extend
реализаций могут смешивать несколько объектов:
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
Единственное, что вам нужно иметь в виду, это не конфликтовать с именами, т. Е. Миксины определяют одно и то же имя некоторых атрибутов или методов, поскольку они будут переопределены. По моему скромному опыту, это не проблема, и если это произойдет, это признак неправильного дизайна.
Принцип замещения Лискова (LSP)
Дядя Боб определяет это просто:
Производные классы должны быть заменяемыми для их базовых классов.
Этот принцип старый, на самом деле определение дяди Боба не дифференцирует принципы, поскольку делает LSP по-прежнему тесно связанным с OCP, поскольку в приведенном выше примере стратегии используется тот же супертип ( IBehavior
). Итак, давайте посмотрим на его первоначальное определение Барбары Лисков и посмотрим, сможем ли мы найти что-то еще об этом принципе, которое выглядит как математическая теорема:
Здесь требуется что-то вроде следующего свойства подстановки: если для каждого объекта o1
типа S
существует объект o2
типа, T
такой, что для всех программ, P
определенных в терминах T
, поведение P
неизменяется, когда o1
подставляется, o2
то S
является подтипом T
.
Давай пожмем на это какое-то время, обратите внимание, поскольку в нем вообще не упоминаются занятия. В JavaScript вы можете следовать LSP, хотя он явно не основан на классах. Если ваша программа имеет список хотя бы из пары объектов JavaScript, которые:
- должен быть рассчитан таким же образом,
- имеют такое же поведение, и
- иначе каким-то образом совершенно разные
... тогда объекты рассматриваются как имеющие один и тот же "тип", и это не имеет значения для программы. Это по сути полиморфизм . В общем смысле; вам не нужно знать фактический подтип, если вы используете его интерфейс. OCP ничего не говорит об этом прямо. Это также фактически указывает на ошибку проектирования, которую делают большинство начинающих программистов:
Всякий раз, когда вы чувствуете желание проверить подтип объекта, вы, скорее всего, делаете это НЕПРАВИЛЬНО.
Хорошо, так что это может быть не всегда неправильно, но если у вас есть желание сделать какую-то проверку типов с помощью instanceof
или перечислений, вы можете сделать программу немного более сложной для себя, чем это необходимо. Но это не всегда так; быстрые и грязные взломы, чтобы заставить вещи работать, - нормальная уступка, на мой взгляд, если решение достаточно мало, и если вы практикуете беспощадный рефакторинг , оно может улучшиться, как только изменения потребуют этого.
Есть несколько способов обойти эту «ошибку проектирования», в зависимости от актуальной проблемы:
- Суперкласс не вызывает предварительные условия, а заставляет вызывающего сделать это.
- В суперклассе отсутствует универсальный метод, который нужен вызывающей стороне.
Обе они являются общими «ошибками» дизайна кода. Существует несколько различных способов рефакторинга , таких как метод подтягивания или рефакторинг для шаблона, такого как шаблон Visitor .
Мне действительно очень нравится шаблон Visitor, так как он может позаботиться о больших спагетти if-операторов, и его проще реализовать, чем то, что вы думаете о существующем коде. Скажем, у нас есть следующий контекст:
public class Context {
public void doStuff(string query) {
// outcome no. 1
if (query.Equals("Hello")) {
System.out.println("Hello world!");
}
// outcome no. 2
else if (query.Equals("Bye")) {
System.out.println("Good bye cruel world!");
}
// a change request may require another outcome...
}
}
// usage:
Context c = new Context();
c.doStuff("Hello");
// prints "Hello world"
c.doStuff("Bye");
// prints "Bye"
Результаты оператора if могут быть переведены в их собственных посетителей, поскольку каждый из них зависит от того или иного решения и какого-либо кода для запуска. Мы можем извлечь их так:
public interface IVisitor {
public bool canDo(string query);
public void doStuff();
}
// outcome 1
public class HelloVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Hello");
}
public void doStuff() {
System.out.println("Hello World");
}
}
// outcome 2
public class ByeVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Bye");
}
public void doStuff() {
System.out.println("Good bye cruel world");
}
}
На этом этапе, если программист не знает о шаблоне Visitor, он вместо этого реализует класс Context, чтобы проверить, имеет ли он какой-то определенный тип. Поскольку классы Visitor имеют логический canDo
метод, разработчик может использовать этот метод, чтобы определить, является ли объект правильным для выполнения работы. Класс контекста может использовать всех посетителей (и добавлять новых) следующим образом:
public class Context {
private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();
public Context() {
visitors.add(new HelloVisitor());
visitors.add(new ByeVisitor());
}
// instead of if-statements, go through all visitors
// and use the canDo method to determine if the
// visitor object is the right one to "visit"
public void doStuff(string query) {
for(IVisitor visitor : visitors) {
if (visitor.canDo(query)) {
visitor.doStuff();
break;
// or return... it depends if you have logic
// after this foreach loop
}
}
}
// dynamically adds new visitors
public void addVisitor(IVisitor visitor) {
if (visitor != null)
visitors.add(visitor);
}
}
Оба шаблона следуют OCP и LSP, однако оба они указывают на разные вещи о них. Так как же выглядит код, если он нарушает один из принципов?
Нарушение одного принципа, но следование другому
Есть способы нарушить один из принципов, но все же нужно придерживаться другого. Приведенные ниже примеры кажутся надуманными, не зря, но на самом деле я видел, как они всплывают в рабочем коде (и даже хуже):
Следует OCP, но не LSP
Допустим, у нас есть данный код:
public interface IPerson {}
public class Boss implements IPerson {
public void doBossStuff() { ... }
}
public class Peon implements IPerson {
public void doPeonStuff() { ... }
}
public class Context {
public Collection<IPerson> getPersons() { ... }
}
Этот кусок кода следует принципу открытого-закрытого. Если мы вызываем метод контекста GetPersons
, мы получим группу людей со своими реализациями. Это означает, что IPerson закрыта для модификации, но открыта для расширения. Однако, когда мы используем его, все становится темным:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// now we have to check the type... :-P
if (person instanceof Boss) {
((Boss) person).doBossStuff();
}
else if (person instanceof Peon) {
((Peon) person).doPeonStuff();
}
}
Вы должны сделать проверку типов и преобразование типов! Помните, как я упоминал выше, что проверка типов - это плохо ? о нет! Но не бойтесь, как уже упоминалось выше, либо проведите рефакторинг подтягивания, либо внедрите шаблон Visitor. В этом случае мы можем просто выполнить рефакторинг после добавления общего метода:
public class Boss implements IPerson {
// we're adding this general method
public void doStuff() {
// that does the call instead
this.doBossStuff();
}
public void doBossStuff() { ... }
}
public interface IPerson {
// pulled up method from Boss
public void doStuff();
}
// do the same for Peon
Преимущество теперь в том, что вам не нужно больше знать точный тип после LSP:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// yay, no type checking!
person.doStuff();
}
Следует за LSP, но не OCP
Давайте посмотрим на некоторый код, который следует за LSP, но не OCP, он немного надуманный, но потерпите меня на этом, это очень тонкая ошибка:
public class LiskovBase {
public void doStuff() {
System.out.println("My name is Liskov");
}
}
public class LiskovSub extends LiskovBase {
public void doStuff() {
System.out.println("I'm a sub Liskov!");
}
}
public class Context {
private LiskovBase base;
// the good stuff
public void doLiskovyStuff() {
base.doStuff();
}
public void setBase(LiskovBase base) { this.base = base }
}
Код выполняет LSP, потому что контекст может использовать LiskovBase, не зная фактического типа. Вы могли бы подумать, что этот код также следует за OCP, но посмотрите внимательно, действительно ли класс закрыт ? Что если doStuff
метод сделал больше, чем просто распечатал строку?
Ответ, если он следует за OCP, прост: НЕТ , это не потому, что в этом объектном дизайне мы должны полностью переопределить код чем-то другим. Это открывает червяк для вырезания и вставки, поскольку вам нужно скопировать код из базового класса, чтобы все заработало. doStuff
Метод уверен , открыт для расширения, но он не был полностью закрыт для модификации.
Мы можем применить шаблон шаблон шаблона к этому. Шаблонный шаблонный шаблон настолько распространен в фреймворках, что вы могли использовать его, не зная его (например, компоненты Java-свинга, формы и компоненты c # и т. Д.). Вот один из способов закрыть doStuff
метод для модификации и убедиться, что он остается закрытым, пометив его final
ключевым словом java . Это ключевое слово не позволяет кому-либо в дальнейшем создавать подклассы класса (в C # вы можете использовать sealed
то же самое).
public class LiskovBase {
// this is now a template method
// the code that was duplicated
public final void doStuff() {
System.out.println(getStuffString());
}
// extension point, the code that "varies"
// in LiskovBase and it's subclasses
// called by the template method above
// we expect it to be virtual and overridden
public string getStuffString() {
return "My name is Liskov";
}
}
public class LiskovSub extends LiskovBase {
// the extension overridden
// the actual code that varied
public string getStuffString() {
return "I'm sub Liskov!";
}
}
Этот пример следует за OCP и кажется глупым, что это так, но представьте, что это увеличено с большим количеством кода для обработки. Я продолжаю видеть код, развернутый в производстве, где подклассы полностью переопределяют все, а переопределенный код в большинстве случаев вырезан между вставками. Это работает, но, как и при любом дублировании кода, это также настройка для техобслуживающих кошмаров.
Заключение
Я надеюсь, что все это проясняет некоторые вопросы, касающиеся OCP и LSP и различий / сходств между ними. Их легко отклонить как одно и то же, но приведенные выше примеры должны показать, что это не так.
Обратите внимание, что, собрав сверху пример кода:
OCP - это блокировка рабочего кода, но он все равно остается открытым с некоторыми точками расширения.
Это сделано для того, чтобы избежать дублирования кода путем инкапсуляции кода, который изменяется, как в примере шаблона Template Method. Это также позволяет быстро потерпеть неудачу, поскольку ломать изменения болезненно (то есть менять одно место, ломать его везде). Для поддержания концепции концепция инкапсуляции изменений - это хорошо, потому что изменения всегда происходят.
LSP позволяет пользователю обрабатывать различные объекты, которые реализуют супертип, без проверки того, что это за тип. Это по сути то, что такое полиморфизм .
Этот принцип предоставляет альтернативу для выполнения проверки типов и преобразования типов, которая может выйти из-под контроля по мере роста числа типов и может быть достигнута с помощью рефакторинга подтягивания или применения таких шаблонов, как Visitor.