Чи порушують особливі випадки з запасною системою Принцип заміни Ліскова?


20

Скажімо, у мене є інтерфейс FooInterfaceіз таким підписом:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

І конкретний клас, ConcreteFooякий реалізує цей інтерфейс:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Я хотів би ConcreteFoo::doSomething()зробити щось унікальне, якщо він передається спеціальному типу SomethingInterfaceоб'єкта (скажімо, він називається SpecialSomething).

Це, безумовно, порушення LSP, якщо я посилюю передумови методу або викидаю новий виняток, але чи все-таки це буде порушення LSP, якщо я створюю SpecialSomethingоб'єкти зі спеціальним обкладинкою , забезпечуючи резервний варіант для загальних SomethingInterfaceоб'єктів? Щось на зразок:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}

Відповіді:


19

Це може бути порушенням LSP залежно від того, що ще є в договорі щодо doSomethingметоду. Але це майже напевно запах коду, навіть якщо він не порушує LSP.

Наприклад, якщо частиною договору doSomethingє те, що він зателефонує something.commitUpdates()принаймні один раз перед поверненням, а для особливого випадку він зателефонує commitSpecialUpdates(), то це є порушенням LSP. Навіть якщо SpecialSomething«s commitSpecialUpdates()метод був свідомо розроблений , щоб зробити все з того ж матеріалу , як commitUpdates(), що це просто превентивно злому навколо порушення LSP, і саме свого роду візок , запряжена волами не доведеться робити , якщо один слідував LSP послідовно. Чи щось подібне стосується вашої справи - це щось, що вам доведеться з’ясувати, перевіривши ваш договір на цей метод (будь то явний чи неявний).

Причина цього запаху коду полягає в тому, що перевірка конкретного типу одного з ваших аргументів в першу чергу пропускає точку визначення інтерфейсу / абстрактного типу для нього, а тому, що в принципі ви більше не можете гарантувати, що метод навіть працює (уявіть собі якщо хтось пише підклас SpecialSomethingз припущенням, що commitUpdates()буде викликано). Перш за все, спробуйте зробити так, щоб ці спеціальні оновлення працювали в межах існуючихSomethingInterface; це найкращий можливий результат. Якщо ви дійсно впевнені, що не можете цього зробити, то вам потрібно оновити інтерфейс. Якщо ви не керуєте інтерфейсом, можливо, вам доведеться розглянути можливість створення власного інтерфейсу, який робить те, що ви хочете. Якщо ви навіть не можете придумати інтерфейс, який працює для всіх, можливо, вам слід повністю перенести інтерфейс і скористатися декількома методами, які використовують різні типи конкретних, або, можливо, ще більший рефактор. Ми повинні були б дізнатися більше про магію, яку ви прокоментували, щоб сказати, що з них підходить.


Спасибі! Це допомагає. Гіпотетично метою doSomething()методу було б перетворення типу до SpecialSomething: якщо він отримає, SpecialSomethingвін просто поверне об'єкт немодифікованим, тоді як якщо він отримає загальний SomethingInterfaceоб'єкт, він би запустив алгоритм для його перетворення в SpecialSomethingоб'єкт. Оскільки передумови та передумови залишаються однаковими, я не вважаю, що договір був порушений.
Еван

1
@ Еван О, уау ... це цікавий випадок. Це насправді може бути абсолютно безпроблемним. Єдине, про що я можу подумати - це те, що якщо ви повертаєте існуючий об’єкт замість того, щоб створювати новий об’єкт, можливо, хтось залежить від цього методу, який повертає абсолютно новий об’єкт ... але від того, чи може така річ зламати людей, може залежати мова. Можна чи для кого - то назвати y = doSomething(x), то x.setFoo(3), а потім виявити , що y.getFoo()повертається 3?
Іксрек

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

1

Це не порушення ЛСП. Однак це все ж є порушенням правила "не робити перевірки типу". Краще було б розробити код таким чином, щоб спеціальний виникав природним шляхом; можливо, SomethingInterfaceпотрібен інший член, який міг би це досягти, або, можливо, вам потрібно кудись ввести абстрактну фабрику.

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


1

Ні, скориставшись тим, що даний аргумент не забезпечує лише інтерфейс A, але й A2, не порушує LSP.

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

Шаблони C ++ часто роблять це для забезпечення кращої продуктивності, наприклад, вимагаючи InputIterators, але надаючи додаткові гарантії, якщо вони викликаються з RandomAccessIterators.

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

Скориставшись особливим випадком, часто йде проти DRY (не повторюйте себе), оскільки, можливо, доведеться дублювати код та проти KISS (Keep it Simple), оскільки це складніше.


0

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

Особисто я віддаю перевагу колишньому підходу, хоча хочу, щоб об'єктно-орієнтовані рамки, такі як .NET, дозволяли б інтерфейсам визначати методи за замовчуванням (роблячи більші інтерфейси менш болісними для роботи). Якщо загальний інтерфейс включає необов'язкові методи, то один клас обгортки може обробляти об'єкти з багатьма різними комбінаціями здібностей, одночасно обіцяючи споживачам лише ті здібності, які є в оригінальному обгорнутому об'єкті. Якщо багато функцій розділено на різні інтерфейси, то для кожної різної комбінації інтерфейсів, які, можливо, знадобиться підтримувати загорнуті об'єкти, знадобиться інший об’єкт обгортки.


0

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

Однак тут порушено принцип відкритого закритого типу. Якщо у вас є нова вимога - Робіть SpecialSomething magic - і вам доведеться змінювати існуючий код, то ви напевно порушуєте OCP .

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