LSP проти OCP / Liskov Заміна VS Open Close


48

Я намагаюся зрозуміти тверді принципи ООП і дійшов висновку, що LSP і OCP мають деякі подібності (якщо не сказати більше).

принцип відкритого / закритого типу визначає, що "програмні об'єкти (класи, модулі, функції тощо) повинні бути відкритими для розширення, але закритими для модифікації".

LSP простими словами зазначає, що будь-який примірник Fooможе бути замінений будь-яким екземпляром Bar, похідним від якого, Fooі програма буде працювати так само.

Я не професійний програміст OOP, але мені здається, що LSP можливий лише тоді Bar, коли похідне від нього Fooнічого не змінює, а лише розширює. Це означає, що зокрема програма LSP істинна лише тоді, коли OCP є істинним, а OCP - істинним, лише якщо LSP є істинним. Це означає, що вони рівні.

Виправте мене, якщо я помиляюся. Я дуже хочу зрозуміти ці ідеї. Велике спасибі за відповідь.


4
Це дуже вузьке тлумачення обох понять. Відкрите / закрите можна підтримувати, але все ще порушує LSP. Приклади прямокутника / квадрата або еліпса / кола - хороші ілюстрації. Обидва дотримуються OCP, але обидва порушують LSP.
Джоель Етертон

1
Світ (або принаймні Інтернет) з цього приводу плутається. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Цей хлопець каже, що порушення LSP також є порушенням OCP. А потім у книзі "Проектування програмного забезпечення: теорія та практика" на сторінці 156 автор наводить приклад того, що дотримується OCP, але порушує LSP. Я від цього відмовився.
Manoj R

@JoelEtherton Ці пари порушують лише LSP, якщо вони змінюються. У незмінному випадку похідне Squareвід Rectangleне порушує LSP. (Але це, мабуть, все ще поганий дизайн у непорушному випадку, оскільки у вас можуть бути квадратні Rectangles, що не є, Squareщо не відповідає математиці)
CodesInChaos

Проста аналогія (з точки зору користувача бібліотечного письменника). LSP - це на зразок продажу продукту (бібліотеки), який стверджує, що реалізує 100% сказаного (в інтерфейсі чи посібнику користувача), але насправді не відповідає (або не відповідає сказаному). OCP - це як продаж продукту (бібліотеки), обіцяючи, що його можна буде оновити (розширити), коли з'явиться новий функціонал (наприклад, прошивка), але насправді не можна оновити без фабричного обслуговування.
rwong

Відповіді:


119

Боже, є деякі дивні помилки щодо того, що OCP та LSP, а деякі пояснюються невідповідністю деяких термінологій та заплутаними прикладами. Обидва принципи - це лише "те саме", якщо ви реалізуєте їх однаково. Шаблони зазвичай так чи інакше дотримуються принципів за невеликими винятками.

Відмінності будуть пояснені далі, але спочатку давайте зануримось у самі принципи:

Принцип відкритого закриття (OCP)

За словами дядька Боба :

Ви повинні мати можливість розширити поведінку класів, не змінюючи її.

Зауважте, що слово розширення в цьому випадку не обов'язково означає, що вам слід підкласити фактичний клас, який потребує нової поведінки. Подивіться, як я спочатку згадував невідповідність термінології? Ключове слово extendозначає лише підкласифікацію в Java, але принципи старіші за Java.

Оригінал прийшов від Бертрана Мейєра в 1988 році:

Програмні об'єкти (класи, модулі, функції тощо) повинні бути відкритими для розширення, але закритими для модифікації.

Тут набагато зрозуміліше, що принцип застосовується до програмних об'єктів . Поганим прикладом може бути переосмислення програмного забезпечення, коли ви повністю змінюєте код, а не надаєте деяку точку розширення. Поведінка самого програмного забезпечення має бути розширюваним, і хорошим прикладом цього є реалізація стратегії (оскільки це найпростіший показ групи ІМХО-шаблонів GoF):

// 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інтерфейс.

Тобто контекстний клас закритий для модифікації, але відкритий для розширення . Насправді слід ще один основний принцип, тому що ми ставимо поведінку з об'єктною композицією замість успадкування:

"Вибрана композиція об'єкта " над " успадкуванням класу ". " (Банда з чотирьох 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 також не згадує.

Розширення міксинами замість спадкування

Є й інші способи зробити 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, оскільки він може піклуватися про великі спагеті if-statement, і це простіше в застосуванні, ніж те, що ви думаєте про існуючий код. Скажімо, у нас такий контекст:

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 є булевий 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();
    }
}

Ви повинні зробити перевірку типу та перетворення типів! Пам'ятайте, як я згадував вище, як перевірка типу - це погано ? О ні! Але не бійтеся, як також було сказано вище, або виконайте деякий рефакторинг, або підтягуйте шаблон відвідувачів. У цьому випадку ми можемо просто зробити рефакторинг після підключення загального методу:

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 swing, 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 і здається нерозумним, яким він є, але уявіть, що це масштабується з більшим кодом для обробки. Я продовжую бачити код, розгорнутий у виробництві, де підкласи повністю перекривають усе, а переосмислений код здебільшого вирізається між n-вставленими. Це працює, але як і всі дублювання коду, це також налаштування для кошмарів технічного обслуговування.

Висновок

Я сподіваюся, що все це очищує деякі питання щодо OCP та LSP та відмінності / подібності між ними. Легко відхилити їх як однакові, але наведені вище приклади повинні показувати, що вони не є.

Зауважте, що, збираючи зверху зразок коду:

  • OCP полягає в тому, щоб заблокувати робочий код, але все одно тримайте його відкритим якось із деякими точками розширення.

    Це дозволяє уникнути дублювання коду шляхом інкапсуляції коду, який змінюється, як у прикладі шаблону методу шаблону. Це також дозволяє швидко провалюватися, оскільки порушення змін болючі (тобто змінити одне місце, порушити його всюди в іншому місці). Задля підтримки концепція інкапсуляції змін - це добре, адже зміни завжди відбуваються.

  • LSP полягає в тому, щоб дозволити користувачеві обробляти різні об'єкти, що реалізують супертип, не перевіряючи, який вони фактичний тип. Це суттєво те , про що йдеться у поліморфізмі .

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


7
Це хороше пояснення, оскільки воно не надто спрощує OCP, маючи на увазі, що це завжди означає реалізацію у спадок. Це те спрощення, яке приєднується до OCP та SRP у свідомості деяких людей, коли насправді вони можуть бути двома абсолютно окремими поняттями.
Ерік Кінг

5
Це одна з найкращих відповідей на обмін стеками, яку я коли-небудь бачив. Я б хотів, щоб я міг підняти його 10 разів. Молодці, і дякую за чудове пояснення.
Боб Хорн

Там я додав розмиття на Javascript, який не є мовою програмування на основі класу, але все ще може слідувати LSP та редагувати текст, щоб, сподіваюся, читати більш вільно. Фу!
Спіке

Хоча ваша цитата дядька Боба з LSP правильна (така ж, як і його веб-сайт), чи не повинно бути навпаки? Чи не повинно бути зазначено, що "Базові класи повинні бути замінені на їх похідні класи"? На LSP тест "сумісності" проводиться проти похідного класу, а не базового. Все-таки я не є носієм англійської мови, і я думаю, що можуть бути деякі деталі щодо фрази, яку я можу пропустити.
Альфа

@Alpha: Це гарне запитання. Базовий клас завжди замінюється його похідними класами, інакше успадкування не буде працювати. Компілятор (принаймні в Java та C #) поскаржиться, якщо ви виходите з члена (методу чи атрибута / поля) з розширеного класу, який потрібно реалізувати. LSP покликаний унеможливити додавання методів, доступних лише локально у похідних класах, оскільки це вимагає від користувачів цих похідних класів знати про них. У міру зростання коду такі методи буде важко підтримувати.
Спіке

15

Це те, що викликає багато плутанини. Я вважаю за краще розглядати ці принципи дещо по-філософськи, тому що для них є багато різних прикладів, а іноді конкретні приклади насправді не відображають всієї їх суті.

Що намагається виправити OCP

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

Проблеми з цим є

  1. Це змінює потік існуючого, робочого коду.
  2. Це примушує нове умовне розгалуження у кожному випадку. Наприклад, скажіть, що у вас є список книг, а деякі з них є у продажу, і ви хочете повторити їх надрукувати та роздрукувати їх ціну, так що, якщо вони продаються, ціна на друк буде включати рядок " (ПРОДАЄТЬСЯ)".

Це можна зробити, додавши додаткове поле до всіх книг під назвою "is_on_sale", а потім ви можете перевірити це поле, друкуючи ціну будь-якої книги, або, як альтернатива , ви можете подати книги про продаж книг із бази даних, використовуючи інший тип, який друкує "(ПРОДАЄТЬСЯ)" у ціновому рядку (не ідеальний дизайн, але він забезпечує точку додому).

Проблема з першим, процедурним рішенням, є додатковим полем для кожної книги та надмірною складністю у багатьох випадках. Друге рішення змушує логіку лише там, де це фактично потрібно.

Тепер подумайте про те, що може бути безліч випадків, коли потрібні різні дані та логіка, і ви побачите, чому пам’ятати OCP під час проектування своїх класів або реагувати на зміни вимог - це гарна ідея.

На сьогодні ви маєте отримати головну думку: Спробуйте поставити себе в ситуацію, коли новий код можна реалізувати як поліморфні розширення, а не процедурні модифікації.

Але ніколи не бійтеся аналізувати контекст і дивіться, чи неприємності переважають переваги, адже навіть такий принцип, як OCP, може зробити 20-класовий безлад з 20-рядкової програми, якщо не ставитися до цього уважно .

Що намагається виправити LSP

Всі ми любимо повторне використання коду. Захворювання, яке випливає з цього, полягає в тому, що багато програм не розуміють його повністю, до того моменту, коли вони сліпо розподіляють загальні рядки коду лише для створення нечитабельних складнощів та надмірної щільної зв'язку між модулями, які, окрім кількох рядків коду, не мають нічого спільного, наскільки йде концептуальна робота, яку потрібно виконати.

Найбільшим прикладом цього є повторне використання інтерфейсу . Ви, напевно, були свідками цього самі; клас реалізує інтерфейс не тому, що це його логічна реалізація (або розширення у випадку конкретних базових класів), а тому, що методи, за якими він трапляється оголосити в цей момент, мають правильні підписи, наскільки це стосується.

Але тоді ви стикаєтесь з проблемою. Якщо класи реалізують інтерфейси лише враховуючи підписи методів, які вони декларують, то ви виявляєте, що можете передати екземпляри класів з однієї концептуальної функціональності в місця, де потрібні зовсім інші функціональні можливості, які залежать лише від подібних підписів.

Це не так жахливо, але це викликає багато плутанини, і у нас є технологія, щоб не допустити подібних помилок. Що нам потрібно зробити - це трактувати інтерфейси як протокол API + . API очевидний у деклараціях, а протокол - у існуючих сферах використання інтерфейсу. Якщо у нас є 2 концептуальні протоколи, які мають один і той же API, вони повинні бути представлені у вигляді двох різних інтерфейсів. В іншому випадку ми потрапляємо в ДОМАШИЙ догматизм і, як не дивно, лише створюємо складніше підтримку коду.

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


1
Я підписався просто, щоб мати можливість проголосувати за це, а відповіді Спойке - чудова робота.
Девід Калп

7

З мого розуміння:

OCP каже: "Якщо ви додасте новий функціонал, створіть новий клас, розширивши існуючий, а не змінюючи його".

LSP каже: "Якщо ви створюєте новий клас, розширюючи існуючий клас, переконайтеся, що він повністю взаємозамінний з його базою".

Тому я думаю, що вони доповнюють одне одного, але вони не рівні.


4

Хоча це правда, що OCP і LSP мають відношення до модифікації, про тип модифікації OCP йдеться не про те, про який говорить LSP.

Модифікація щодо OCP - це фізична дія коду написання розробника в існуючому класі.

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

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


2

LSP простими словами зазначає, що будь-який екземпляр Foo можна замінити будь-яким екземпляром Bar, який походить від Foo без втрати функціональності програми.

Це неправильно. LSP заявляє, що клас Bar не повинен вводити поведінку, що не очікується, коли код використовує Foo, коли Bar походить від Foo. Це не має нічого спільного з втратою функціональності. Ви можете видалити функціональність, але лише тоді, коли код за допомогою Foo не залежить від цієї функціональності.

Зрештою, цього, як правило, важко досягти, тому що більшість часу використання коду Foo залежить від усієї його поведінки. Тому його видалення порушує LSP. Але таке спрощення - це лише частина LSP.


Дуже поширений випадок, коли заміщений об’єкт видаляє побічні ефекти : наприклад. фіктивний реєстратор, який нічого не видає, або макетний об’єкт, який використовується при тестуванні.
Марно

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, тому що вона не розширюється без змін. Але немає ознак порушення ЛСП. І навіть не ясно, якщо 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.

Але контракт вимагає, що він toJsonповинен повернути належним чином відформатований JSON. Клас цього не робить. Через це його не можна передати компоненту, який використовує 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 повинно бути відповідне вказівка. У цьому прикладі OCP порушується HumanReadablePlatformSerializer.isMostPopularфункцією, оскільки колись iOS стає найпопулярнішою платформою. Формально це означає, що набір найбільш використовуваних платформ наразі визначений як "Android" і isMostPopularнеадекватно обробляє цей набір даних. Набір даних не фіксується семантично і може вільно змінюватися з часом. HumanReadablePlatformSerializerвихідний код повинен бути оновлений у разі зміни.

Ви також можете помітити порушення єдиної відповідальності в цьому прикладі. Я навмисно зробив це, щоб мати змогу продемонструвати обидва принципи на одній і тій же сутності. Щоб виправити SRP, ви можете витягти isMostPopularфункцію до деякого зовнішнього Helperта додати параметр до PlatformSerializer.toJson. Але це вже інша історія.


0

LSP і OCP - не однакові.

LSP розповідає про правильність програми в такому стані . Якщо екземпляр підтипу порушив би правильність програми, замінивши його кодом для типів предків, то ви продемонстрували порушення LSP. Можливо, вам доведеться змайструвати тест, щоб показати це, але вам не доведеться змінювати базу коду, що лежить в основі. Ви перевіряєте саму програму, щоб побачити, чи відповідає вона LSP.

OCP розповідає про правильність зміни програмного коду, дельти від однієї вихідної версії до іншої. Поведінку не слід змінювати. Його слід лише продовжити. Класичний приклад - додавання поля. Усі існуючі поля продовжують працювати, як і раніше. Нове поле просто додає функціональність. Однак видалення поля, як правило, є порушенням OCP. Тут ви перевіряєте дельту версії програми, щоб побачити, чи відповідає вона OCP.

Отже, це ключова відмінність між LSP і OCP. Перший підтверджує лише кодову базу в такому стані, остання підтверджує лише дельту кодової бази від однієї версії до наступної . Як такі вони не можуть бути однаковими, вони визначаються як перевірка різних речей.

Я надам вам більш офіційний доказ: сказати, що "LSP означає OCP" означатиме дельту (оскільки OCP вимагає іншого, ніж у тривіальному випадку), але LSP не вимагає цього. Отже, це явно помилково. І навпаки, ми можемо спростувати "OCP має на увазі LSP", просто кажучи, що OCP - це твердження про дельти, тому воно нічого не говорить про заяву над програмою на місці. Це випливає з того, що ви можете створити будь-яку дельту, починаючи з будь-якої програми. Вони повністю незалежні.


-1

Я б дивився на це з точки зору клієнта. якщо Клієнт використовує функції інтерфейсу, а внутрішньо ця функція була реалізована класом А. Припустимо, існує клас В, який розширює клас А, то завтра, якщо я видалю клас А з цього інтерфейсу і поставлю клас В, то клас Б повинен також надають ті ж функції для клієнта. Стандартний приклад - клас Дак, який плаває, і якщо ToyDuck розширює Duck, він також повинен плавати і не скаржиться, що не вміє плавати, інакше ToyDuck не повинен мати розширений клас Duck.


Було б дуже конструктивно, якби люди також коментували, хоч і голосували будь-яку відповідь. Зрештою, ми всі тут, щоб ділитися знаннями, і просто винесення судження без належних причин не збирається служити ніякій меті.
AKS

це , здається, не пропонує нічого істотного по точкам зроблених і роз'яснено в попередніх 6 відповідей
комар

1
Здається, ви просто пояснюєте один із принципів, той, що я думаю. Для чого це нормально, але в питанні було задано порівняння / контраст двох різних принципів. Ось, мабуть, тому хтось проти цього.
StarWeaver
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.