LSP vs OCP / Замена Лискова VS Open Закрыть

48

Я пытаюсь понять твердые принципы ООП и пришел к выводу, что у LSP и OCP есть некоторые сходства (если не сказать больше).

принцип открытого / закрытого состояния гласит, что «программные объекты (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации».

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

Я не профессиональный программист ООП, но мне кажется, что LSP возможен только в том случае Bar, если производный от него Fooничего не меняет, а только расширяет. Это означает, что в конкретной программе LSP имеет значение «истина» только тогда, когда OCP имеет значение «истина», а OCP - «истина» только тогда, когда «LSP» имеет значение «истина». Это означает, что они равны.

Поправьте меня если я ошибаюсь. Я действительно хочу понять эти идеи. Большое спасибо за ответ.

Колюня
источник
4
Это очень узкая интерпретация обоих понятий. Открытое / закрытое может поддерживаться, но все еще нарушать LSP. Примеры Rectangle / Square или Ellipse / Circle являются хорошими иллюстрациями. Оба придерживаются OCP, но оба нарушают LSP.
Джоэл Этертон
1
Мир (или, по крайней мере, Интернет) запутался в этом. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Этот парень говорит, что нарушение LSP также является нарушением OCP. А затем в книге «Разработка программного обеспечения: теория и практика» на странице 156 автор приводит пример чего-то, что придерживается OCP, но нарушает LSP. Я отказался от этого.
Маной Р
@JoelEtherton Эти пары нарушают LSP, только если они изменчивы. В неизменном случае производное Squareот Rectangleне нарушает LSP. (Но это, вероятно, все еще плохой дизайн в неизменном случае, поскольку у вас могут быть квадраты Rectangles, Squareкоторые не соответствуют математике)
CodesInChaos
Простая аналогия (с точки зрения автора библиотеки-пользователя). LSP подобен продаже продукта (библиотеки), который утверждает, что реализует 100% того, что говорит (в интерфейсе или руководстве пользователя), но на самом деле этого не делает (или не соответствует тому, что говорится). OCP - это все равно что продавать продукт (библиотеку) с обещанием, что он может быть обновлен (расширен), когда появится новая функциональность (например, прошивка), но на самом деле его невозможно обновить без заводского обслуживания.
13:30

Ответы:

119

Черт возьми, есть некоторые странные заблуждения относительно того, что такое 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.

Spoike
источник
7
Это хорошее объяснение, потому что оно не упрощает OCP, подразумевая, что оно всегда означает реализацию по наследству. Именно такое упрощение объединяет OCP и SRP в сознании некоторых людей, когда на самом деле они могут быть двумя совершенно разными концепциями.
Эрик Кинг,
5
Это один из лучших ответов на обмен стека, который я когда-либо видел. Хотел бы я поднять это 10 раз. Молодцы, и спасибо за отличное объяснение.
Боб Хорн
Там я добавил рекламный ролик на Javascript, который не является языком программирования на основе классов, но все еще может следовать LSP и отредактировал текст, чтобы он, надеюсь, читал более свободно. Уф!
Спойк
Хотя ваша цитата из дяди Боба из LSP верна (так же, как и его веб-сайт), не должно ли быть наоборот? Разве это не должно утверждать, что «Базовые классы должны быть заменяемыми для их производных классов»? На LSP тест «совместимости» проводится с производным классом, а не с базовым. Тем не менее, я не являюсь носителем английского языка, и я думаю, что могут быть некоторые детали о фразе, которую я могу пропустить.
Альфа
@ Альфа: Это хороший вопрос. Базовый класс всегда можно заменить его производными классами, иначе наследование не будет работать. Компилятор (по крайней мере в Java и C #) будет жаловаться, если вы пропускаете член (метод или атрибут / поле) из расширенного класса, который необходимо реализовать. LSP предназначен для того, чтобы удерживать вас от добавления методов, которые доступны только локально в производных классах, поскольку это требует, чтобы пользователь этих производных классов знал о них. По мере роста кода такие методы будет сложно поддерживать.
Спойк
15

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

Что OCP пытается исправить

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

Проблемы с этим

  1. Это изменяет поток существующего, рабочего кода.
  2. Это вызывает новое условное ветвление в каждом случае. Например, скажем, у вас есть список книг, и некоторые из них продаются, и вы хотите перебрать все из них и напечатать их цену, так что, если они в продаже, в напечатанную цену будет включена строка " (В ПРОДАЖЕ)".

Вы можете сделать это, добавив дополнительное поле ко всем книгам с именем «is_on_sale», а затем вы можете проверить это поле при печати цены любой книги, или, в качестве альтернативы , вы можете создать экземпляр книги в продаже из базы данных, используя другой тип, который печатает «(В ПРОДАЖЕ)» в ценовой категории (не идеальный дизайн, но он дает смысл домой).

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

Теперь рассмотрим тот факт, что может быть много случаев, когда требуются разные данные и логика, и вы поймете, почему стоит помнить об OCP при разработке классов или реагировании на изменения требований.

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

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

Что пытается исправить LSP

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

Самый большой пример этого - повторное использование интерфейса . Вы, вероятно, были свидетелями этого сами; класс реализует интерфейс не потому, что его логическая реализация (или расширение в случае конкретных базовых классов), а потому, что методы, которые он объявляет в этой точке, имеют правильные сигнатуры в той степени, в которой это его касается.

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

Это не так уж и ужасно, но это вызывает много путаницы, и у нас есть технология, позволяющая нам не допускать подобных ошибок. Нам нужно рассматривать интерфейсы как протокол API + . API очевиден в декларациях, а протокол очевиден в существующих применениях интерфейса. Если у нас есть 2 концептуальных протокола, которые используют один и тот же API, они должны быть представлены как 2 разных интерфейса. В противном случае мы попадаем в догматизм DRY и, по иронии судьбы, создаем только сложный код для поддержки.

Теперь вы должны быть в состоянии понять определение отлично. В LSP говорится: не наследуйте от базового класса и не реализуйте функциональность в тех подклассах, с которыми другие места, которые зависят от базового класса, не будут ладить.

Ям Маркович
источник
1
Я подписался только для того, чтобы проголосовать, и ответы Спойка - отличная работа.
Дэвид Калп
7

Из моего понимания:

OCP говорит: «Если вы добавите новую функциональность, создайте новый класс, расширяющий существующий, а не меняя его».

LSP говорит: «Если вы создаете новый класс, расширяющий существующий класс, убедитесь, что он полностью взаимозаменяем с его базой».

Поэтому я думаю, что они дополняют друг друга, но они не равны.

henginy
источник
4

Хотя верно то, что OCP и LSP связаны с модификацией, о типе изменений, о котором говорит OCP, - не тот, о котором говорит LSP.

Изменение в отношении OCP - это физическое действие разработчика, пишущего код в существующем классе.

LSP имеет дело с модификацией поведения, которую приносит производный класс по сравнению с его базовым классом, и изменением времени выполнения программы, которое может быть вызвано использованием подкласса вместо суперкласса.

Таким образом, хотя они могут выглядеть похожими на расстоянии OCP! = LSP. На самом деле, я думаю, что они могут быть единственными 2 твердыми принципами, которые не могут быть поняты друг с другом.

guillaume31
источник
2

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

Это не верно. LSP утверждает, что класс Bar не должен вводить поведение, которое не ожидается, когда код использует Foo, когда Bar является производным от Foo. Это не имеет ничего общего с потерей функциональности. Вы можете удалить функциональность, но только когда код, использующий Foo, не зависит от этой функциональности.

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

Euphoric
источник
Очень распространенный случай, когда замещенный объект устраняет побочные эффекты : например. фиктивный логгер, который ничего не выводит, или фиктивный объект, используемый при тестировании.
Бесполезно
0

Об объектах, которые могут нарушать

Чтобы понять разницу, вы должны понимать предметы обоих принципов. Это не какая-то абстрактная часть кода или ситуации, которая может нарушать какой-либо принцип. Это всегда какой-то конкретный компонент - функция, класс или модуль - который может нарушать OCP или LSP.

Кто может нарушать LSP

Можно проверить, не нарушен ли LSP, только когда есть интерфейс с некоторым контрактом и реализация этого интерфейса. Если реализация не соответствует интерфейсу или, вообще говоря, контракту, то LSP нарушается.

Простейший пример:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

В контракте четко указано, что addObjectследует добавить свой аргумент в контейнер. И CustomContainerявно нарушает этот контракт. Таким образом CustomContainer.addObjectфункция нарушает LSP. Таким образом, CustomContainerкласс нарушает LSP. Наиболее важным последствием является то, что CustomContainerне может быть передано fillWithRandomNumbers(). Containerне может быть заменено на CustomContainer.

Имейте в виду, очень важный момент. Не весь этот код нарушает LSP, а именно CustomContainer.addObjectи вообще CustomContainerнарушает LSP. Когда вы заявляете, что LSP нарушается, вы всегда должны указывать две вещи:

  • Сущность, которая нарушает LSP.
  • Контракт, который нарушается организацией.

Вот и все. Просто договор и его реализация. Понижение в коде ничего не говорит о нарушении LSP.

Кто может нарушать OCP

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

Звучит сложно. Давайте попробуем простой пример:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

Набор данных - это набор поддерживаемых платформ. PlatformDescriberявляется компонентом, который обрабатывает значения из этого набора данных. Добавление новой платформы требует обновления исходного кода PlatformDescriber. Таким образом, PlatformDescriberкласс нарушает OCP.

Другой пример:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

«Набор данных» - это набор каналов, в которые следует добавить запись журнала. Loggerявляется компонентом, отвечающим за добавление записей во все каналы. Добавление поддержки другого способа ведения журнала требует обновления исходного кода Logger. Таким образом, Loggerкласс нарушает OCP.

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

Раздвигая пределы

Теперь сложная часть. Сравните приведенные выше примеры со следующими:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

Вы можете думать, что translateToRussianнарушает OCP. Но на самом деле это не так. GregorianWeekDayимеет определенный лимит ровно 7 дней в неделю с точными именами. И важно то, что эти пределы семантически не могут меняться со временем. В григорианской неделе всегда будет 7 дней. Всегда будет понедельник, вторник и т. Д. Этот набор данных семантически фиксирован. Не возможно, что translateToRussianисходный код потребует изменений. Таким образом, OCP не нарушается.

Теперь должно быть ясно, что исчерпывающее switchутверждение не всегда является признаком нарушения OCP.

Различия

Теперь почувствуй разницу:

  • Предметом LSP является «реализация интерфейса / контракта». Если реализация не соответствует контракту, это нарушает LSP. Не важно, может ли эта реализация изменяться со временем или нет, расширяемая она или нет.
  • Предметом OCP является «способ реагирования на изменение требований». Если для поддержки нового типа данных требуется изменить исходный код компонента, который обрабатывает эти данные, то этот компонент нарушает 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

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

Здесь HumanReadablePlatformSerializerне требуется никаких изменений при добавлении новой платформы. Таким образом, следует OCP.

Но контракт требует, чтобы toJsonJSON возвращал правильно отформатированный файл. Класс не делает этого. Из-за этого он не может быть передан компоненту, который использует PlatformSerializerдля форматирования тела сетевого запроса. Таким образом HumanReadablePlatformSerializerнарушает LSP.

Следуйте LSP, но не OCP

Некоторые модификации предыдущего примера:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

Сериализатор возвращает правильно отформатированную строку JSON. Таким образом, здесь нет нарушения LSP.

Но есть требование, что если платформа используется наиболее широко, то в JSON должна быть соответствующая индикация. В этом примере HumanReadablePlatformSerializer.isMostPopularфункция OCP нарушается функцией, потому что когда-нибудь iOS станет самой популярной платформой. Формально это означает, что набор наиболее используемых платформ пока определен как «Android» и isMostPopularнеадекватно обрабатывает этот набор данных. Набор данных не является семантически фиксированным и может свободно изменяться со временем. HumanReadablePlatformSerializerисходный код должен быть обновлен в случае изменения.

Вы также можете заметить нарушение Единой Ответственности в этом примере. Я намеренно сделал так, чтобы иметь возможность продемонстрировать оба принципа на одном предмете. Для исправления SRP вы можете извлечь isMostPopularфункцию из внешнего интерфейса Helperи добавить параметр в PlatformSerializer.toJson. Но это другая история.

mekarthedev
источник
0

LSP и OCP - это не одно и то же.

LSP говорит о правильности программы в ее нынешнем виде . Если экземпляр подтипа нарушит корректность программы при замене на код для типов предков, то вы продемонстрировали нарушение LSP. Возможно, вам придется смоделировать тест, чтобы показать это, но вам не придется менять основную базу кода. Вы проверяете саму программу, чтобы увидеть, соответствует ли она LSP.

OCP говорит о правильности изменений в программном коде, дельта из одной исходной версии в другую. Поведение не должно быть изменено. Это должно быть только продлено. Классический пример - добавление полей. Все существующие поля продолжают работать как прежде. Новое поле просто добавляет функциональность. Однако удаление поля, как правило, является нарушением OCP. Здесь вы проверяете дельту версии программы, чтобы увидеть, соответствует ли она OCP.

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

Я приведу более формальное доказательство: сказать «LSP подразумевает OCP» означало бы дельту (потому что OCP требует одно, а не в тривиальном случае), а LSP не требует. Так что это явно неверно. И наоборот, мы можем опровергнуть «OCP подразумевает LSP», просто сказав, что OCP - это утверждение о дельтах, поэтому в нем ничего не говорится о выражении над программой на месте. Это следует из того факта, что вы можете создать ЛЮБУЮ дельту, начиная с ЛЮБОЙ программы на месте. Они полностью независимы.

Брэд Томас
источник
-1

Я бы посмотрел на это с точки зрения клиента. если Клиент использует функции интерфейса и внутренне эта функция была реализована классом А. Предположим, есть класс B, который расширяет класс A, тогда завтра, если я удалю класс A из этого интерфейса и добавлю класс B, тогда класс B должен также предоставить те же функции для клиента. Стандартным примером является класс Duck, который плавает, и если ToyDuck расширяет Duck, то он также должен плавать и не жаловаться, что не умеет плавать, в противном случае ToyDuck не должен был бы расширять класс Duck.

АКС
источник
Было бы очень конструктивно, если бы люди оставляли комментарии и во время голосования за любой ответ. В конце концов, мы все здесь для того, чтобы делиться знаниями, и простое суждение без надлежащей причины не будет служить какой-либо цели.
AKS
кажется, это не дает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 6 ответах
комнат
1
Похоже, вы просто объясняете один из принципов, L, я думаю. Для чего это все нормально, но вопрос задан для сравнения / противопоставления двух разных принципов. Наверное, поэтому кто-то и проголосовал за это.
StarWeaver,