Все, що ви хотіли знати про принципи SOLID. Частина третя: LSP
Вітаю, друзі! З вами знову Сергій Немчинський — програміст з понад
Нагадую, що я прагну зробити світ програмування кращим. Для цього потрібно більше професійних розробників, які пишуть чистий та якісний код. Щоб сприяти цьому, я працюю над серією статей про принципи SOLID та покращення світу програмування з їх допомогою.
Знов-таки нагадую, що критика принципів SOLID та концепції чистого коду має право на існування, в тому числі в коментарях. Але перш ніж щось критикувати, треба з’ясувати, що саме ми критикуємо та чому. Так створюються об’єктивні професійні погляди.
Сьогодні пропоную вашій увазі третю статтю циклу, яка описує принцип Liskov Substitution Principle, він же LSP.
В попередніх серіях
Для тих, хто пропустив перші дві статті, коротко нагадаю їх зміст. Принципи SOLID були зібрані й систематизовані Робертом Мартіном. SOLID — це акронім, кожна літера — початок назви одного з принципів.
Ми вже розглянули принцип єдиної відповідальності (Single Responsibility Principle) та принцип відкритості-закритості (Open-Closed Principle) — літери S та O відповідно. Настав час для Liskov Substitution Principle, літера L. Цей принцип, названий на честь Барбари Лісков, є єдиним з п’яти принципів SOLID, що носить ім’я людини.
Історія та формулювання
Спочатку трохи про авторку принципу. Барбара Лісков — видатна американська науковиця та винахідниця у галузі комп’ютерних наук, перша жінка-професорка інженерного факультету MIT — інституту технологій в Массачусетсі, який вважається головним технічним ВНЗ у світі. Лісков розробила CLU — революційну мову програмування, що вплинула на розвиток Java, C++ та інших сучасних мов. Також вона є лауреаткою премії Тюрінга 2008 року, це аналог Нобелівки в програмуванні.
Окрім того, що наукові роботи Лісков змінили підхід до проєктування програмного забезпечення, вона ще створила «ефект Скаллі» в IT-індустрії, надихнувши ціле покоління дослідниць і розробниць.
І ось ця видатна жінка сформулювала принцип підстановки, або Substitution, таким чином:
У тому випадку, якщо Q від X властивість вірована щодо об’єктів X деякого типу T, то властивість Q від Y також буде вірною відносно ряду об’єктів Y, які відносяться до типу S, при цьому S підтип якогось типу T.
Сподіваюсь, що серед читачів статті знайдуться ті, кому це формулювання зрозуміло без додаткових пояснень. Я до них не належу. На моє (і не лише моє) щастя Роберт Мартін, який систематизував принципи SOLID, запропонував простіше формулювання:
«Функції, які використовують базовий тип, повинні мати можливість використовувати підтипи базового типу, не знаючи про це.»
Ще простіше можна сказати: поведінка в похідних класах не повинна суперечити поведінці, заданій базовим класом.
Принцип підстановки Лісков на прикладі без коду
Уявіть собі компанію з автоматизованим складом, де використовуються різні типи транспортних роботів. У компанії є базова модель робота-транспортувальника зі стандартними функціями: «підняти вантаж», «рухатися до точки», «опустити вантаж». Усі ці функції чітко описані в інструкції з експлуатації, і всі працівники знають, як цими роботами керувати.
Компанія купила нову, покращену модель роботів-транспортувальників з додатковим функціоналом. Якщо ПЗ нових роботів повністю відповідає принципу підстановки Лісков, то все працює, як раніше. Наприклад, набір команд «підняти вантаж» — «рухатися до точки А» — «опустити вантаж» призводить до того, що робот підіймає, переміщує та опускає вантаж в точці А.
А якщо принцип підстановки порушено, при тому самому наборі команд робот спочатку запросить задати калібрування вантажу, потім зупиниться на метр лівіше точки А, а вантаж не просто опустить, а штовхне уперед, чим призведе до нещасного випадку на виробництві.
Якщо в цьому місці вам стало боляче, ви теж стикалися з продуктами, в яких принцип Лісков було порушено. Це нормально, і саме тому ми про це говоримо — таких порушень має бути якнайменше.
Приклади порушення LSP в житті програмістів
Сподіваюсь, основну ідею принципу LSP ви вже зрозуміли. Тепер розкажу, як цей принцип порушується в програмуванні й до чого це призводить.
Невпроваджені методи
У вас є клас, який описує якесь зовнішнє джерело даних, наприклад, якусь Legacy-систему. У класу є купа методів, які використовує ваш клієнтський код. Ви пишете Unit Test, для чого створюєте тестовий об’єкт, він же mock. Вам, звичайно ж, лінь прописувати всі ці методи, тому ви від них успадковуєтесь, але всередині пишете: throw new NotImplementedException, позначаючи свій дочірній клас як виключення. І у вас все працює добре.

А потім приходить інший програміст, створює новий код, бере ваш mock, і пише свій юніт-тест. І у нього все падає з NotImplementedException. Програміст лається, і ми його дуже розуміємо, бо це реально мєрзость. Це звісно не така велика проблема, тому що це юніт-тест і програміст одразу побачить в чому проблема, але якщо так зробити з класом бойового коду, то проблема може вилізти в зовсім несподіваному місці.
Якщо ви робите непрацездатним один із методів вашого базового класу, ви порушуєте LSP, і ви створюєте дуже велику підлянку іншим програмістам, які будуть працювати з цією системою, тому що ніхто не знає, де цей метод вибухне. Не робіть так.
Додаткові вимоги до клієнтського коду
Є багато ситуацій, коли у вас дочірній клас працює не зовсім так, як базовий. Наприклад, якщо для використання якогось методу в дочірньому класі необхідно спочатку викликати додатковий метод ініціалізації, якого не було в базовому класі — це змушує клієнтський код знати про особливості реалізації підкласу.
Це також порушення LSP. Ваш клас повинен використовуватися так само.
Повернення NULL
Базовий клас повертає реальні дані, а в дочірньому класі ви подумали, що ось цей метод ніхто викликати не буде, і повертаєте null. А потім інший програміст якимсь чином таки звернувся до вашого дочірнього класу, отримав помилку і не знає, що з нею робити.
Якщо базовий клас повертає повний набір даних, а дочірній клас повертає неповні дані або null — це також порушення LSP. Код, що використовує базовий клас, очікує отримати нормальні дані та не готовий до обробки null або неповних даних. Та і взагалі ніколи не повертайте NULL, але про це окремо, якщо вам буде цікаво.
Прямокутник і квадрат
Це класичний приклад, який використовують для ілюстрації LSP. З точки зору геометрії квадрат є особливим випадком прямокутника. Тому логічно було б зробити клас квадрата нащадком класу прямокутника, і більшість програмістів так і робить.
Однак це порушує LSP, бо поведінка квадрата відрізняється від очікуваної поведінки прямокутника. У прямокутника можна незалежно змінювати довжину і ширину, а у квадрата при зміні однієї сторони повинна автоматично змінюватися й інша, щоб зберегти властивість рівності всіх сторін.
Клієнтський код, який очікує, що змінюється тільки одна сторона, буде працювати неправильно з квадратом. Наприклад, якщо встановити спочатку одну сторону 10, а потім іншу 5, то у випадку з квадратом або ви отримаєте те саме not implemented, або значення 10 для другої сторони буде перезаписане новим 5. Це може призвести до несподіваних результатів, наприклад, при обчисленні площі. Тобто або ви отримаєте помилку, або невірні дані.
Як не порушувати LSP
Тільки не кажіть, що ви не використовуєте успадкування, тому й про LSP вам думати не потрібно. Забудьте про це. Якщо ви не використовуєте успадкування, ви не використовуєте об’єктноорієнтоване програмування. Ви криворукий лайнокодер, що пише процедурний код. Мені за вас соромно.
А для всіх інших ось п’ять простих правил, які допоможуть не порушувати принцип підстановки:
- Переконайтеся, що всі методи дочірнього класу відповідають контрактам базового класу. Якщо ви не можете реалізувати метод, можливо, вам не слід використовувати наслідування.
- Не кидайте винятки в методах, які перевизначаєте, якщо базовий клас не передбачає цих винятків.
- Не повертайте null, якщо базовий клас повертає об’єкт. Краще використовуйте шаблон Null Object.
- Ретельно продумуйте ієрархію класів. Наприклад, у випадку з прямокутником і квадратом, краще не робити квадрат нащадком прямокутника, а створити загальний базовий клас «Фігура» і від нього успадкувати обидва класи.
- Пишіть тести, які перевіряють відповідність LSP для ваших класів.
Коли можна порушувати LSP
Це ваша улюблена частина: якщо не можна, але дуже хочеться, то трошки можна. Як і будь-який принцип, LSP вимагає розумного дотримання. У деяких випадках повністю дотримуватися принципу може бути складно.
Наприклад, при створенні mock-об’єктів для тестування, коли базовий клас має багато методів, а вам потрібна лише мала їх частина. Добре, тоді пропишіть тут exception. Але пам’ятайте, що так ви трішечки налайнокодили. Тому обов’язково документуйте, що це mock-клас, який порушує LSP і не повинен використовуватися в бойовому коді. Ваші колеги, які працюватимуть з цією системою, вам подякують, і ваша карма не постраждає.
Приклад практичного застосування принципу підстановки Лісков
Продовжимо наш серіал про Петра та Василя з їхньою системою датчиків. Тепер датчики реалізують інтерфейс Sensor з методами getValue(), calibrate() та getUnit(). Система чудово працює з усіма типами датчиків через цей єдиний інтерфейс.
Одного дня Василь приносить новий датчик температури EconomySensor, який споживає менше енергії. Керівництво у захваті, новий датчик миттєво впроваджено. Але після впровадження система починає давати збої: з’явилися помилки під час калібрування, нульові значення на екранах та неправильні розрахунки через різний формат даних про температуру.
Колега Василя Петро сідає за аналіз і з’ясовує, що код датчика:
- Викидає виняток у методі calibrate() замість виконання калібрування.
- Вимагає попереднього виклику warmUp() перед getValue(), інакше повертає null.
- Може повертати значення у «C» або «F» з getUnit() залежно від стану.
Що робить Петро? Пояснює Василю, що його економний датчик порушує принцип Лісков, бо не може коректно замінити базовий тип. І що тепер, нові датчики забирати? Та ні. Досить перепланувати рішення:
- Переробити calibrate(), щоб виконував пусту операцію без помилок.
- Автоматично викликати warmUp() всередині getValue().
- Стандартизувати getUnit(), щоб завжди повертав «C».
Для розширених можливостей можна створити окремий інтерфейс AdvancedSensor з додатковими методами, які працюють з розширеними даними і не порушують основний контракт.
Щасливий Василь отримує бонус, задоволений клієнт підраховує економію, а втомлений Петро витирає піт з лоба.
Висновок
Принцип підстановки Лісков — це важливий принцип об’єктноорієнтованого програмування, який допомагає створювати гнучкі та надійні системи. Дотримуючись цього принципу, ви забезпечуєте, що ваші класи можуть бути легко розширені без порушення існуючого коду.
Нові, розширені версії ПЗ можуть додавати функціональність, але не можуть змінювати базову поведінку, на яку всі покладаються. Якщо ви використовуєте наслідування, переконайтеся, що дочірні класи можуть повністю замінити батьківські класи без зміни очікуваної поведінки програми. Думайте над своїми ієрархіями класів, не кидайте винятки в перевизначених методах, не повертайте null і не змушуйте клієнтський код вивчати внутрішню структуру ваших класів.
І, звісно, пишіть в коментарях, що ви про це все думаєте.
50 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівКорисна стаття. Подєкував
Якщо ви використовуєте успадкування, ви з великою ймовірністю порушуєте SRP, або ви просто намагаєтесь реалізувати якісь книжні паттерни, без особливої потрібності для вирішення самої задачі.
Ем. В сенсі? Якщо немає успадкування, то половина поліморфізму вам недоступна. Ну і де тут тоді ООП?
Поліморфізм може бути доступний через інтерфейси, як у Go. Обмежень немає ніяких, бо будь який код з успадкуванням можна переписати через інтерфейси та агрегацію.
Порушення SRP у тому, що підклас успадковує усе, включно з тим, що йому не потрібно. Після трьох-чотирьох рівнів успадкування вже виникає божественний монстр.
Що там в результаті? Сотня методів і властивостей?
З радістю б не використовував, бо в світі нема нічого більш вульгарного, зіпсованого і незрозумілого ніж ооп.
Так
Переписує, а потім знову переписує, а потім пропустивши через нейронку переписує втретє*, і не процедруний, а ооп*
Хтось нав’язує ООП, хтось — функціональне програмування. Але навіть у FAANG це не догма. У багатьох сучасних напрямках — Data Science, AI , cloud рішеннях, data engineering — чудово працюють рішення навіть без ООП. І цілі команди інженерів при цьому не вважаються лайнокодерами, і нікому за них не соромно.
Як на мене Ваше твердження звучить агресивно й однобоко. Поки ви шеймите українських розробників, інші вже масштабують продукти — з ООП і без нього.
Інструмент обирають під задачу, а не навпаки.
Two minutes later
О — об’єктивність.
Вайбкодерам не потрібне LSP
Визначення Барбари Лісков — максимально точне і, що цікаво, не прив’язане до ООП. Воно працює для будь-якої формальної системи з підтипами. Наприклад, у Pascal:
Range2 — підтип Range1, і будь-яке твердження істинне для Range1, буде істинне і для Range2. Це і є суть підстановки — незалежно від ООП, класів.
Якщо брати визначення Мартіна, то це вже флейм. Формально кажучи, будь яка ООП мова програмування з підтримкою або наслідування, або через інтерфейси, дозволяє нам підставляти замість класу його наслідника, або замість інтерфейсу його реалізацію. Тому в принципі можна сказати, що виконання LSP контролює компілятор.
Якщо брати формулювання автора «підтип має поводитися як суперклас»... Але що таке «поведінка»? Автор статті не дає формального визначення. Що робить продовження дуже маніпулятивним: (1) читач сам підставить своє бачення, що призведе до хибного відчуття «розімуння»; (2) можна самому гратися та кожного абзацу розуміти по іншому. Мо може бути поведінкою? Не кидати виключень, кидати лише дозволені, не мати побічних ефектів, зберігати інваріанти, ... Усе це часто виходить за межі типових засобів контролю в мовах загального вжитку. У функціональних мовах, таких як Haskell чи PureScript, побічні ефекти контролюються типами. В Ada можна явно задавати інваріанти. Що ілюструє ставлення більшості до цього принципу — пофік.
Приклад з NotImplementedException — чудова ілюстрація того, що «порушення LSP» це ретроспективна оцінка на основі очікувань, а не формальних вимог. Причому досить суб’єктивних. Якщо документація чи інтерфейс обіцяє наявність реалізації — тоді справді є порушення очікуваної поведінки, але я такого ніколи не бачив. А якщо ні, починається суб’єктивне: чи NotImplementedException — це неправильна поведінка? Для когось це чесний спосіб сказати «не готово», для іншого — зрада.
У підсумку LSP — це не скільки правило про кодування, скільки правило розуміння документації, там де це стосується підтипів та супертипів. А порушення LSP це невідповідність кода та документації. Що приклад з роботами ілюструє.
Ви мене випередили, з питанням
Бо не тільки у даному випадку, взагалі тексти про ООП спираються на цей термін, який тлумачиться коли як, ким як хочеться, а особливо — молодими програмістами, на основі їх побутового уявлення та «здорового глузду»
Додам ще до ваших зауважень
Все більш менш класно виглядає у «підручниках» по ООП коли розгладають тільки один крок наслідування.
Але ж, все стає дуже цікавим коли маємо:
А
Б від А, О від Б
В від А, Ю від В
то, якщо все робили «правільно», то ми можемо:
Замість А використати Ю?
Ю замість Б?
І головне — якщо ми можемо — то навіщо ми взагалі робили субтип?
Чим він — відрізняються А, Б, В, О, Ю
і т.д.
Холівари йдуть десятиліття :)
Бо ООП — то філософія, а не математика.
Як на мене, принцип L — найпрозоріший із SOLID. Його можна перевірити експериментально: код, що працює з A, не має бути в змозі визначити, що саме йому підсунули — A, Б чи Ю (без рефлексії чи чогось на кшталт if (obj instanceof A)).
Старий код, який працює з A і був написаний ще до появи Б та В, взагалі не повинен цікавитися, які саме з’явилися підкласи та з якою метою. Він має просто працювати з ними як з A — це можливо лише якщо підкласи нічого не порушують із того, що було гарантовано в A. Це як Open-Closed, поширений на підкласи.
Тоді виходить що instanceof, рефлексія це порушення L. Ну... не дуже поширений погляд.
Якщо вважати, що підходить не нащадок у явній ієрархії, а будь-який тип, що реалізує потрібні методи — тобто фактично качина типізація — і це перевіряється рефлексією — то мабуть норм. (Я б звичайно вимагав трейта, але не в усіх мовах таке є.)
Так у тому ж і задача наслідування — залишати публічний інтерфейс — без змін.
Він і не може — «цікавитись». Він написаний для А. І використовує А саме тому що воно робить те що цьому коду потрібно.
Якщо ж ми підсовуємо Б — то код і не помітить. Але, Б чимось же відрізняється від А, чи не так?
Заради цієї різниці і був створений — Б.
Та він так і буде пробувати працювати, бо він вже ж написаний, для А.
А що було — гарантовано в А?
Беремо будь-який public-метод A і замінюємо його в Б на «заглушку», яка завжди кидає ексепшн. Клієнтський код може визначити, підсунули йому A чи Б, якщо викличе цей метод — отже, LSP порушено.
Б має додаткові методи, яких немає в A, але клієнтський код, написаний для A, нічого про них не знає і не може їх викликати.
який повертає string
і замінюємо його в Б який повертає string
просто, у А повертав стрінгу яка починалася з «http://»
ну а Б стрінга повертається що починається «https://»
ну а у В, стрінга повертається починаючись з «sftp://»
Ніяких додаткових методів.
От беремо, і підставляємо замість А, — Б або В.
Клієнтський код просто викликає — objAtype.getString()
Буде працювати?
Якщо ні — то чому, яким чином ми порушили принцип Лісков, чи Open-Closed?
SOLID не про те, чи буде працювати чи ні.
порушили Лісков тим, що Б повертає щось інше ніж А (https замість http).
можливо все ж таки варто створити новий окремий метод в Б, який повертатиме https. а старий код нехай і далі працює з http
Вище ви писали:
І далі я відповів що це — опис наслідування, а не «принципа Лісков».
Взагалі то було означено — string
Де сталося порушення?
але який тоді код буде працювати з цим новим методом?
Я просто навів дуже очевидний приклад про — суто академічно-педагогічне значення принципа Лісков.
На практиці — максимум за що можна боротися — це щоб об’єкти які знаходяться на одному й тому ж рівні наслідування можна було використовувати один замість іншого.
Якщо ж ми хочемо замість базового типа пхати його нащадків, то — ой. Буде купа коду, незрозуміло з якою метою, і дешевше буде
композицію замість наслідування використати
або виокремити у чисті інтерфейси необхідне узагальнення.
Як я джунів питав:
то що наслідуємо — інтерфейс чи імплементацію?
Але ж саме цього й вимагає принцип Лісков за визначенням. Ви просто з цим не погоджуєтесь. Можливо працюєте з легасі-кодом, у якому це систематично не дотримувалося, і вам тепер здається, що дотриматись цього принципу неможливо, бо для цього доведеться переписати все з нуля.
чому — я :)
кодова база масово з цим не погоджується.
Роками, всюди, на купі мов.
З 90их те бачу, коли ООП стало мейнстрімом.
Можливо ви просто, як більшість — не замислювались над кодом, який читаєте, який пишите, і академічністю — яку самі, постійно посилаєте на йух.
Він нічого в ООП не вимагає, крім того що і так є:
При наслідуванні не змінюй сігнатури методів :)
про те й мій тролінг :)
А про легасі, ну так
є код який працює — і він легасі. Бо любий код що працює — легасі.
і є код який не працює, бо він не написаний, або існує тільки в підручниках і навчальних курсах.
А, то ви з фракції «LSP уже перевірено компілятором»
Це вже більше схоже на зведення особистих рахунків із Б. Лісков — то ваші справи, не втручатимусь :)
але у самому принципі написано саме те, що там написано
Якшо «поведінку» замінити на «контракт» вам буде ок?
а що такє — “контракт”?
In Object-Oriented Programming (OOP), a “contract” refers to a set of agreements or specifications defining how objects should interact and behave.
(Gemini)
ну вам ок дефініції у стилі:
uk.wikipedia.org/wiki/Сепулька
Друже, якшо трішки (реально децл) знати латину, або навіть англійську tidbit better — то жарт пана Станіслава не тільки стане зрозумілим, а й заграє новими кольорами:
www.merriam-webster.com/dictionary/sepulchre
да, прикольно.
те, у вас вкрай рідкісний скіл. Самі вивчали, чи десь?
то як, з знанням латини та англійської:
зможете надати дефініцію «контракт» без використання «поведінки»?
*sigh*
контракт на поведінку ok?
от то шо ви бачите в доках, коли дивитесь як юзать компонент
метод робить те-то
повертає оце о
а якшо отако о, то повертає оте о
а ще може викинути такий, такий і отакий ексепшени
Але що таке «поведінка»?
dou.ua/...rums/topic/54053/#2972440
У вас проблеми з розумінням самих основ ООП, мені не здалося?
Знайдіть визначення Об’єкта в цій парадигмі і уважно прочитайте — там це є.
та да, 30+ років у розробці, а все ніяк не розберуся :D
Тобто ви, знавець латини, а відповісти не можете ;)
ох ви не повірите скільки я такого бачив
можу
я не хочу за вас це розжовувати
підкажу що навіть з вікіпедії можна почать (англомовної), там дость пристойні визначення
реально, освіжіть знання, вони в вас за «30+ років» пилом припали
Ну от я й бачу, на вашому прикладі :D
Тобто — не можете :)
Це ж в мене старе питання до знавців ООП :D
Але ви — так і не втопопали що там у вікіпедії написано.
То може вам слід освіжити знання? ;)
«сам дурак» — так побєдітє!
ну це ж ви взялися відповісти на питання що такє «поведінка» і не змогли навіть у вікіпедії прочитати :)
Nah. Bullcrap.
... бо «дозволяє» != «контролює»
Як ви, затятий монадщик, допускаєте подібні школярські логічні факапи — це сором.
Let ϕ(x) be a property provable about objects x of type T.
Виділене.
Це генералістське визначення Лісков, але ж в ООП поведінка об’єкта (конракт на дії які виконоуть його методи) також його property.
Oh really.
А я чомусь постійно в документації бачу список ексепшенів які метод може викидувать... шо я роблю не так?
Трохи копіпасти, усе інше один в один:
class A { public: virtual void doSomething() { std::cout << "A::doSomething\n"; } }; class B : public A { public: void doSomething() override { std::cout << "B::doSomething\n"; } };ось без наслідування:
struct IA { virtual void doSomething() = 0; }; class A : public IA { public: void doSomething() override { std::cout << "A::doSomething\n"; } }; class B : public IA { A a; public: void doSomething() override { std::cout << "B:: before A\n"; a.doSomething(); std::cout << "B:: after A\n"; } };не дуже зрозумів шо це доводить, вибачте
От я писав:
Нескладно бачити, що це просто нестрога ілюстрація цього визначення без математичної формалістики, просто приклади таких властивостей, де ϕ(x) це x немає побічних ефектів, або для x є інваріант. Даля provable = можуть бути перевірені компілятором. Тобто таке визначення як раз проілюстровано, на що я зазначив, що «виходить за межі типових засобів контролю в мовах загального вжитку».
Так, я це вже зазначив:
Трохи поясню ці слова. Якщо в документації є список ексепшенів, і припустимо ми маємо порушення LSP: підтип кидає виключення NotImplementedException відсутнє в документації. Як виправити? (1) просто додати в документацію «для mock об’єктів що використовуються у unit-тестах дозволено кидати NotImplementedException» і все буде відповідно документації, або (2) зробити реалізації без NotImplementedException.
та не треба compliance за будь-яку ціну
більше того, юніт-тести — НЕ частина архітектури, НЕ частина апплікації, тощо
то яка різниця шо саме моки недомокали?
поганий приклад, корочє, я не розумію чого ви чипляєтеся за нього
Варто відмітити, що принцип підставивши типів значно ширше за об’єктно орієнтовне, та навіть і ща імперативне програмування.
P.S. Більшість новачкі
в власне, не дуже розуміються на абстракції тип данних взагалі. Тому цей принцип з SOLID викликає особливо жорсткий конфуз, на співбесідах.
Ось є приклад
void method1(Collection<String> names) { names.clear(); } void method2() { method1(List.of()); }Хто тут порушує LSP?Хто має виправляти помилку?
Тут LSP не порушує нічого. І з точки зору трансляції — це взагалі валідна программа, яка реалізує безмістовний алгоритм. Фактично і помилки ніякої немає. List тип данних, що розширює базовий тип Collection чистий LSP — усе транслюватиметься.
А типів типу void* чи обгорток типу std::any, або union, паскалевских variant, ML монад в Java подібному прикладі
, що ви написали немає зовсім.
List.of() повертає незмінний об’єкт, при спробі змінити незмінний обʼєкт викидається ексепшн.
Це валідна программа із безмістовним алгоритмом, усеодно. А тут тему що ви підіймаєте — це супер старий холівар про доцільність exceptions. Google скажімо якраз через от такі штуки дуже не любить exceptions, та беззаперечні переваги в них усеодно є. Тому і священні війни.
Ситуація коли программа реалізує не той алгоритм який хотів програміст, або що частіше, не той який потрібен користувачу ПЗ (юзер, замовник і т.д) взагалі лежить в іншій плоскості, в контролі якості і науково доказовому підході до розробки ПЗ (тестування та QA в простонародії).
BTW У того же Мартіна Дядька Боба, це в принципі перша глава в Clean Architecture. А потім вже пояснення SOLID та усього іншого.
Контракт методу:
static <E> List<E> of()Компілятор виведе шо E is String, нічо ніде не порушиться.
Якщо не порушиться, то, певно, працювати теж буде добре.
Тут порушення не в коді, порушення принципу Лісков в самих класах.
так а де, там же ж нічого не наслідувано?
нє, ну як ***ня запроєктована — вона так і працює
давайте за приклад шось менш з атсральних планів більш з реальних — поговорим
Це зменшений приклад реального коду — у реальному між «method2» та «method1» було десь ~ 10 інших методів, по ~ 200 рядків.
Звісно.
Joshua Bloch визнав що начхав на LSP, і тому воно досі працює саме так.
крива Java