Це порушення принципу заміни Ліскова?


132

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

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

Тепер при виклику Close()Завдання є ймовірність, що виклик не вдасться, якщо це статус ProjectTaskіз запущеним статусом, коли він не був би базовим завданням. Але це ділові вимоги. Це повинно провалитися. Чи можна це вважати порушенням принципу заміни Ліскова ?


14
Ідеально підходить до Т-прикладу порушення заміни лескова. Не використовуйте тут спадщину, і у вас все буде добре.
Джиммі Хоффа

8
Ви можете змінити його на public Status Status { get; private set; }:; інакше Close()метод можна обійти.
Робота

5
Можливо, це лише цей приклад, але я не бачу матеріальної користі для дотримання LSP. Для мене це рішення у питанні більш чітке, просте для розуміння та легше у обслуговуванні, ніж одне відповідає LSP.
Бен Лі

2
@BenLee Це не простіше в обслуговуванні. Це виглядає лише тому, що ви бачите це ізольовано. Коли система велика, переконайтесь, що підтипи Taskне вносять химерних несумісностей у поліморфний код, про який відомо Taskлише велика справа. LSP не примха, але введено саме для того, щоб допомогти у підтримці ремонту у великих системах.
Андрес Ф.

8
@BenLee Уявіть, що у вас є TaskCloserпроцес, який closesAllTasks(tasks). Цей процес очевидно не намагається знайти винятки; зрештою, це не є частиною явного договору Task.Close(). Тепер ви запроваджуєте ProjectTaskі раптом TaskCloserпочинаєте викидати (можливо, без обробки) винятки. Це велика справа!
Андрес Ф.

Відповіді:


173

Так, це порушення ЛСП. Принцип заміщення Ліскова вимагає цього

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

Ваш приклад порушує першу вимогу, посилюючи передумову для виклику Close()методу.

Ви можете виправити це, вивівши посилену попередню умову на найвищий рівень ієрархії спадкування:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

Зазначаючи, що виклик Close()дійсний лише в тому стані, коли CanClose()повернення trueви вносите, попередня умова застосовується як до, Taskтак і до ProjectTask, виправляючи порушення LSP:

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (Status == Status.Started) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}

17
Мені не подобається дублювання цього чека. Я вважаю за краще кидати винятки в Task.Close та видаляти віртуальний із Close.
Ейфорія

4
@Euphoric Це правда, якщо Closeперевірка на найвищому рівні зробить перевірку, і додавання захищеного DoCloseбуде правильною альтернативою. Однак я хотів залишитися якомога ближче до прикладу ОП; вдосконалення при цьому - окреме питання.
dasblinkenlight

5
@Euphoric: Але зараз немає відповіді на питання "Чи можна це завдання закрити?" не намагаючись його закрити. Це зайво змушує використовувати винятки для контролю потоку. Я визнаю, однак, що такого роду речі можна зайняти занадто далеко. Зайняте занадто далеко, подібне рішення може в кінцевому підсумку призвести до підприємницького безладу. Незалежно від того, питання ОП вражає мене як принципи, тому відповідь на вежу із слонової кістки дуже доречна. +1
Брайан

30
@Brian CanClose все ще є. Ще можна викликати, щоб перевірити, чи можна закрити завдання. Слід також зателефонувати в пункті Закрити.
Ейфорія

5
@Euphoric: Ах, я неправильно зрозумів. Ви маєте рацію, це робить набагато більш чистим рішення.
Брайан

82

Так. Це порушує LSP.

Моя пропозиція - додати CanCloseметод / властивість до базового завдання, тож будь-яке завдання може визначити, чи можна закрити завдання в цьому стані. Це також може навести причину. І видаліть віртуальну з Close.

На основі мого коментаря:

public class Task {
    public Status Status { get; private set; }

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}

3
Дякую за це, ви взяли приклад dasblinkenlight ще на одному етапі, але мені подобалося його пояснення та виправдання. Вибачте, я не можу прийняти 2 відповіді!
Пол Т Девіс

Мені цікаво знати, чому підпис є публічним віртуальним bool CanClose (з String причини) - використовуючи, ви просто захищаєте майбутнє? Або є щось більш тонке, чого мені не вистачає?
Reacher Gilt

3
@ReacherGilt Я думаю, ви повинні перевірити, що робити / відмовитись та прочитати мій код ще раз. Ви розгублені. Просто "Якщо завдання не вдається закрити, я хочу знати, чому".
Ейфорія

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

1
І чи добре посилити передумови для властивості CanClose? Тобто додавання умови?
Іван V

24

Принцип заміщення Ліскова визначає, що базовий клас повинен бути замінений будь-яким з його підкласів, не змінюючи жодного з бажаних властивостей програми. Оскільки лише ProjectTaskвиняток викликає, коли закривається, програма повинна бути змінена таким чином, щоб вона була доступною для цього, і вона повинна ProjectTaskвикористовуватися для заміни Task. Тож це порушення.

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


Я використовую c #, що, на мою думку, не має такої можливості, але я знаю, що це робить Java.
Пол Т Девіс

2
@PaulTDavies Метод можна прикрасити тим, які винятки він викидає, msdn.microsoft.com/en-us/library/5ast78ax.aspx . Ви помічаєте це, коли наведіть курсор на метод із бібліотеки базового класу, ви отримаєте список винятків. Він не застосовується, але він все-таки дає зрозуміти абоненту.
Деспертар

18

Порушення ЛСП вимагає трьох сторін. Тип T, підтип S і програма P, яка використовує T, але отримує екземпляр S.

У вашому запитанні передбачено T (Завдання) та S (ProjectTask), але не P. Отже, ваше запитання неповне, і відповідь кваліфікована: Якщо існує P, який не очікує винятку, для цього P у вас є LSP порушення. Якщо кожен P очікує на виняток, то порушення LSP немає.

Тим НЕ менше, ви робите мати SRP порушення. Те, що стан завдання може бути змінено, і політика того, що певні завдання в певних штатах не повинні бути змінені в інших штатах, є двома дуже різними обов'язками.

  • Відповідальність 1: Представити завдання.
  • Відповідальність 2: Реалізуйте політику, яка змінює стан завдань.

Ці дві відповідальності змінюються з різних причин і тому повинні бути в окремих класах. Завдання повинні обробляти факт завдання, а також дані, пов'язані з завданням. TaskStatePolicy повинен обробляти спосіб переходу завдань із стану в стан у заданій програмі.


2
Обов'язки значною мірою залежать від домену та (у цьому прикладі) того, наскільки складні стани завдання та його зміни. У цьому випадку немає жодних вказівок на таке, тому немає проблеми з SRP. Що стосується порушення LSP, я вважаю, що всі ми припускали, що абонент не очікує винятку, і програма повинна показувати розумне повідомлення замість того, щоб потрапляти в помилковий стан.
Ейфорія

Unca 'Bob відповідає? "Ми не гідні! Ми не гідні!". У будь-якому випадку ... Якщо кожен P очікує на виняток, то порушення LSP немає. Але якщо ми визначимо, що екземпляр T не може викинути OpenTaskException(підказку, підказку), і кожен P очікує на виняток, то що це говорить про інтерфейс коду, а не про його реалізацію? Про що я говорю? Не знаю. Мені просто заграло, що я коментую відповідь Унси Боба.
radarbob

3
Ви вірні, що для доказу порушення LSP потрібні три об’єкти. Однак порушення LSP існує, якщо існує будь-яка програма P, яка була б правильною за відсутності S, але не вдається з додаванням S.
kevin cline

16

Це може бути, а може і не бути порушенням LSP.

Серйозно. Вислухай мене.

Якщо ви дотримуєтеся LSP, об'єкти типу ProjectTaskповинні поводитись так, як Taskочікується, що вони поводяться.

Проблема вашого коду полягає в тому, що ви не задокументували поведінку об'єктів типу Task. Ви написали код, але жодних контрактів. Я додам контракт на Task.Close. Залежно від контракту, який я додаю, код для ProjectTask.Closeабо не відповідає LSP.

З огляду на наступний контракт на Task.Close, код для ProjectTask.Close не відповідає LSP:

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

З огляду на наступний контракт на Task.Close, код для ProjectTask.Close дійсно слідувати LSP:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Методи, які можуть бути відмінені, повинні бути задокументовані двома способами:

  • "Поведінка" документує те, на що може покладатися клієнт, який знає об'єкт одержувача, є Task, але не знає, для якого класу це прямий екземпляр. Він також повідомляє дизайнерам підкласів, які переосмислення є розумними, а які нерозумними.

  • "Поведінка за замовчуванням" документує те, на що може покладатися клієнт, який знає, що об'єкт одержувач є прямим екземпляром Task(тобто, що ви отримуєте, якщо використовуєте new Task(). Він також повідомляє дизайнерам підкласів, яка поведінка буде успадкована, якщо вони не будуть перевизначити метод.

Тепер мають відбутися такі відносини:

  • Якщо S є підтипом T, документоване поведінка S повинно уточнити задокументовану поведінку Т.
  • Якщо S є підтипом (або рівним) T, поведінка коду S повинно уточнити задокументовану поведінку Т.
  • Якщо S є підтипом (або рівним) T, поведінка S за замовчуванням S повинна уточнити задокументовану поведінку Т.
  • Фактична поведінка коду для класу повинна уточнювати його задокументовану поведінку за замовчуванням.

@ user61852 підкреслив, що ви можете вказати в підписі методу, що він може створити виняток, і, просто зробивши це (щось, що не має дійсного коду ефекту), ви більше не порушуєте LSP.
Пол Т Девіс

@PaulTDavies Ви маєте рацію. Але в більшості мов підпис не є гарним способом заявити, що рутина може кинути виняток. Наприклад, в ОП (на C #, я думаю) друга реалізація Closeкидає. Тож підпис оголошує, що може бути викинуто виняток - це не говорить про те, що цього не буде. Ява робить кращу роботу в цьому плані. Тим не менш, якщо ви заявляєте, що метод може оголосити виняток, ви повинні задокументувати обставини, за яких він може (або буде). Тож я стверджую, що для впевненості в порушенні LSP нам потрібна документація поза підписом.
Теодор Норвелл

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

Гарна відповідь, але й інші відповіді хороші. Вони роблять висновок, що базовий клас не кидає винятку, оскільки в цьому класі немає нічого, що показує ознаки цього. Тому програма, яка використовує базовий клас, не повинна готуватися до винятків.
inf3rno

Ви праві, що список виключень має бути десь задокументований. Я думаю, що найкраще місце в коді. Тут є відповідне запитання: stackoverflow.com/questions/16700130/… Але ви можете це зробити і без приміток, тощо ... також просто напишіть щось на зразок if (false) throw new Exception("cannot start")базового класу. Компілятор видалить його, і все ще код містить те, що потрібно. Btw. у нас все ще є порушення LSP з цими
обхідними шляхами

6

Це не є порушенням принципу заміни Ліскова.

Принцип заміщення Ліскова говорить:

Нехай д (х) є властивістю доказово про об'єкти х типу Т . Нехай S є підтипом T . Тип S порушує Принцип заміщення Ліскова, якщо існує об'єкт y типу S , такий, що q (y) є недоступним.

Причина, чому ваша реалізація підтипу не є порушенням принципу заміни Ліскова, досить проста: нічого не можна довести щодо того, що Task::Close()насправді робить. Звичайно, ProjectTask::Close()кидає виняток коли Status == Status.Started, але так може бути Status = Status.Closedв Task::Close().


4

Так, це порушення.

Я б запропонував вам мати свою ієрархію назад. Якщо не кожен Taskє закритим, то close()не належить Task. Можливо, вам потрібен інтерфейс, CloseableTaskякий всі не ProjectTasksможуть реалізувати.


3
Кожне завдання є закритим, але не за будь-яких обставин.
Пол Т Девіс

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

Якщо Taskсама не реалізує, CloseableTaskто вони роблять десь небезпечний виступ, щоб навіть зателефонувати Close().
Том G

@TomG цього я боюся
Джиммі Хоффа

1
Вже є державна машина. Об'єкт не можна закрити, оскільки він знаходиться в неправильному стані.
Каз

3

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

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


1

Я пропускаю тут важливу річ, пов’язану з LSP та Design by Contract - за попередніми умовами, це той, хто телефонує, відповідальність за те, щоб переконатися, що передумови виконуються. Викликаний код в теорії DbC не повинен перевіряти передумову. У контракті повинно бути вказано, коли завдання може бути закрите (наприклад, CanClose повертає True), і тоді код виклику повинен забезпечити дотримання умови, перш ніж викликати Close ().


У контракті повинно бути визначено будь-яку поведінку бізнесу. У цьому випадку, що Close () створить виняток, коли викликається запущеним ProjectTask. Це постумова (він говорить про те, що відбувається після виклику методу), а його виконання - відповідальність за названий код.
Гойо

@Goyo Так, але, як говорили інші, виняток піднімається в підтипі, який посилив попередню умову і тим самим порушив (мається на увазі) контракт, що виклик Close () просто закриває завдання.
Езоела Вакка

Яка передумова? Я не бачу жодної.
Гойо

@Goyo Перевірте прийняту відповідь, наприклад :) У базовому класі Close немає жодних передумов, він викликається і закриває завдання. Однак у дитини є передумова про те, що статус не починається. Як зазначали інші, це більш сильні критерії, і поведінка, таким чином, не піддається заміні.
Езоела Вакка,

Неважливо, я знайшов передумову у питанні. Але тоді немає нічого поганого (DbC-мудрого), коли викликаний код перевіряє передумови та збільшує винятки, коли вони не виконуються. Його називають "оборонним програмуванням". Крім того, якщо є постумова, яка вказує, що відбувається, коли попередня умова не виконується, як у цьому випадку, реалізація повинна перевірити попередню умову для того, щоб забезпечити виконання цієї умови.
Гойо

0

Так, це явне порушення ЛСП.

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

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

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