Чому багато розробників програмного забезпечення порушують принцип відкритого / закритого типу?


74

Чому багато розробників програмного забезпечення порушують принцип відкритого / закритого типу , змінюючи багато речей, як перейменування функцій, які порушують додаток після оновлення?

Це питання стрибає мені в голову після швидкої та безперервної версії в бібліотеці React .

Кожен короткий період я помічаю багато змін у синтаксисі, назвах компонентів, ... тощо

Приклад у наступній версії React :

Нові попередження про депресію

Найбільша зміна полягає в тому, що ми вилучили React.PropTypes і React.createClass у власні пакети. Обидва досі доступні через головний об'єкт React, але за допомогою будь-якого буде введено одноразове попередження про депрекацію на консоль у режимі розробки. Це дозволить надалі оптимізувати розмір коду.

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


  • Чи розглядаються ці зміни як порушення цього принципу?
  • Як початківець щось на кшталт React , як я дізнаюся це завдяки цим швидким змінам у бібліотеці (це так засмучує)?

6
Це очевидно приклад його спостереження , і ваше твердження «стільки» є необґрунтованим. Проекти Lucene та RichFaces є відомими прикладами та API портами COMM COMM, але я не можу придумати жодних інших без проблем. І чи справді React є "великим розробником програмного забезпечення"?
користувач207421

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

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

Спадщина смокче. 30-річний досвід показує, що вам слід повністю скинути спадщину і почати свіже, в будь-який час. Сьогодні всі мають зв'язок скрізь у будь-який час, тому спадщина сьогодні абсолютно абсолютно не має значення. остаточним прикладом було "Windows проти Mac". Microsoft традиційно намагається "підтримувати спадщину", ви бачите це багато в чому. Apple завжди казала "F- - - You" застарілим користувачам. (Це стосується всього, від мов до пристроїв до ОС.) Насправді Apple була абсолютно правильною, а MSFT був абсолютно неправильним, простим та простим.
Fattie

4
Тому що є абсолютно нульові «принципи» та «дизайнерські зразки», які працюють 100% часу в реальному житті.
Матті Вірккунен

Відповіді:


148

Відповідь ІМХО ЖакБ, хоча містить багато правди, вказує на принципове непорозуміння ОКП. Якщо чесно, ваше питання вже виражає це непорозуміння - перейменування функцій порушує сумісність назад , але не OCP. Якщо зламати сумісність здається необхідним (або підтримувати дві версії одного компонента, щоб не порушувати сумісність), OCP вже був порушений!

Як вже зазначав Йорг У Міттаг у своїх коментарях, принцип не говорить про те, що "ви не можете змінити поведінку компонента" - він говорить, що слід намагатися розробити компоненти таким чином, щоб вони були відкриті для повторного використання (або розширення). декількома способами, не потребуючи модифікацій. Це можна зробити за допомогою надання правильних "точок розширення", або, як зазначає @AntP, "шляхом декомпозиції структури класу / функції до тієї точки, де кожна натуральна точка розширення є за замовчуванням". IMHO, що слідує за OCP, не має нічого спільного з "збереженням старої версії без змін для зворотної сумісності" ! Або, цитуючи коментар @ Дерека Елкіна нижче:

OCP - це поради щодо написання модуля [...], а не щодо впровадження процесу управління змінами, який ніколи не дозволяє модулям змінюватися.

Хороші програмісти використовують свій досвід для розробки компонентів з урахуванням «правильних» точок розширення (або - ще краще - таким чином, що штучні точки розширення не потрібні). Однак, щоб зробити це правильно і без зайвого переобладнання, потрібно заздалегідь знати, як можуть виглядати майбутні випадки використання вашого компонента. Навіть досвідчені програмісти не можуть заздалегідь заглянути в майбутнє і знати всі майбутні вимоги. І тому іноді сумісність із зворотним рівнем потрібно порушувати - незалежно від того, скільки точок розширення має ваш компонент або наскільки добре він відповідає OCP щодо певних типів вимог, завжди буде вимога, яку неможливо легко реалізувати без зміни компонент.


14
Найбільшою причиною ІМО "порушення" OCP є те, що для її належного виконання потрібно багато зусиль. Ерік Ліпперт має чудову публікацію в блозі про те, чому багато класів .NET Framework, здається, порушують OCP.
BJ Майєрс

2
@BJMyers: дякую за посилання. Джон Скіт має чудовий пост про OCP, оскільки він дуже схожий на ідею захищеної зміни.
Doc Brown

8
ЦЕ! OCP говорить, що ви повинні написати код, який можна змінити, не торкаючись! Чому? Тож вам доведеться лише тестувати, переглядати та компілювати його лише один раз. Нова поведінка повинна виходити з нового коду. Не накручуючи старий перевірений код. А як щодо рефакторингу? Добре рефакторинг - це явне порушення OCP! Ось чому гріх писати код, думаючи, що ви просто переробляєте його, якщо ваші припущення зміниться. Немає! Покладіть кожне припущення у свій власний маленький ящик. Якщо це неправильно, не фіксуйте цю скриньку Напишіть нове. Чому? Тому що вам може знадобитися повернутися до старого. Коли ви це зробите, було б добре, якби це все-таки працювало.
candied_orange

7
@CandiedOrange: дякую за ваш коментар. Я не бачу рефакторингу та OCP настільки навпаки, як ви це описуєте. Для написання компонентів, які слідують за OCP, часто потрібно кілька циклів рефакторингу. Мета повинна бути складовою, яка не потребує модифікацій для вирішення цілої "родини" вимог. Тим не менш, не слід додавати довільних точок розширення до компонента "про всяк випадок", що занадто легко призводить до переобладнання. Посилатися на можливість рефакторингу може бути кращою альтернативою цьому у багатьох випадках.
Док Браун

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

67

Принцип відкритого / закритого характеру має переваги, але він має і деякі серйозні недоліки.

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

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

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

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

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

Мабуть, розробники React вважають, що не варто витрачати кошти у складності та розбитті коду, щоб суворо слідувати принципу відкритого / закритого типу.

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

(Моє тлумачення принципу засноване на принципі відкритого закриття Роберта К. Мартіна)


37
"В принципі в основному сказано, що ви не можете змінювати поведінку компонента. Натомість вам слід надати новий варіант компонента з потрібною поведінкою та зберегти стару версію без змін для зворотної сумісності." - Я не згоден з цим. Принцип говорить, що слід розробити компоненти таким чином, щоб не було потреби змінювати його поведінку, оскільки ви можете розширити їх, щоб робити те, що ви хочете. Проблема полягає в тому, що ми ще не розгадали, як це зробити, особливо з мовами, які зараз широко використовуються. Проблема вираження є однією частиною…
Jörg W Mittag

8
... що, наприклад. Ні Java, ні C♯ не мають рішення для Expression. Haskell і Scala роблять, але їх база користувачів набагато менша.
Йорг W Міттаг

1
@Giorgio: У Haskell рішення - це типи класів. У Scala рішення - це імпліцити та об'єкти. На жаль, наразі немає посилань. Так, мультиметоди (насправді вони навіть не повинні бути "мульти", це, швидше, необхідний "відкритий" характер методів Ліспа) - також можливе рішення. Зауважте, що існує декілька фраз фрази проблеми вираження, тому що зазвичай документи написані таким чином, що автор додає обмеження до проблеми вираження, що призводить до того, що всі існуючі на даний момент рішення стають недійсними, а потім показує, як його власні…
Йорг W Міттаг

1
… Мова може навіть вирішити цю «складнішу» версію. Наприклад, Вадлер спочатку формулював проблему вираження не лише про модульне розширення, але і про статично безпечне модульне розширення. Common Lisp мультиметод однак це НЕ статичний безпечні, вони тільки динамічно безпечно. Тоді Одерський ще більше посилив це, сказавши, що він повинен бути модульно статично безпечним, тобто безпеку слід статично перевіряти, не дивлячись на всю програму, лише дивлячись на модуль розширення. Це фактично не можна зробити з класами типів Haskell, але це можна зробити за допомогою Scala. І в…
Jörg W Mittag

2
@Giorgio: Так. Те, що змушує мультиметоди Common Lisp вирішувати EP, насправді не є багаторазовою розсилкою. Справа в тому, що методи відкриті. У типовому FP (або процедурному програмуванні) дискримінація типу прив’язана до функцій. У типових OO методи прив’язуються до типів. Поширені методи Lisp відкриті , їх можна додавати до класів після факту та в інший модуль. Саме ця особливість робить їх корисними для вирішення ЕП. Наприклад, протоколи Clojure - це разова відправка, але також вирішує ЕП (до тих пір, поки ви не наполягаєте на статичній безпеці).
Йорг W Міттаг

20

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

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

Відомий приклад цього можна знайти в диспетчері пам'яті Windows 95. У рамках маркетингу для Windows 95 було заявлено, що всі програми Windows 3.1 працюватимуть в Windows 95. Майкрософт фактично придбав ліцензії на тисячі програм для тестування їх у Windows 95. Одним із проблемних випадків був Сім Сіті. У Сім-Сіті насправді була помилка, яка змусила його написати в нерозподілену пам’ять. У Windows 3.1, без «належного» диспетчера пам'яті, це був незначний штучний пропуск. Однак у Windows 95 менеджер пам'яті вирішив би це і спричинить помилку сегментації. Рішення? У Windows 95, якщо назва вашої програми simcity.exe, ОС фактично полегшить обмеження менеджера пам'яті, щоб запобігти помилки сегментації!

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

Більшість програмного забезпечення сьогодні, особливо з відкритим кодом, дотримується загальної розслабленої версії принципу відкритого / закритого типу. Дуже часто бачити, як відкриті / закриті суттєво слідують за незначними випусками, але відмовилися від великих релізів. Наприклад, Python 2.7 містить безліч «поганих варіантів» з Python 2.0 та 2.1 днів, але Python 3.0 проніс їх усіх. (Крім того , перехід від Windows 95. кодового в кодову Windows NT , коли вони випустили Windows 2000 побив усі види речей, але це ж означає , що ми ніколи не повинні мати справу з менеджером пам'яті перевіряючи назва програми , щоб вирішити поведінку!)


Це досить чудова історія про SimCity. У вас є джерело?
BJ Майєрс

5
@BJMyers Це стара історія, Джоел Сполекий згадує її наприкінці цієї статті . Я спочатку читав це як частину книги про розробку відеоігор років тому.
Корт Аммон

1
@BJMyers: Я впевнений, що вони мали схожі "хаки" для десятків популярних додатків.
Doc Brown

3
@BJMyers є багато подібних матеріалів, якщо ви хочете добре прочитати, перейдіть до блогу The Old New Thing від Raymond Chen , перегляньте тег History або шукайте "сумісність". Згадується безліч казок, включаючи щось помітно близьке до вищезгаданого випадку SimCity - Addentum: Chen не любить називати імена винними.
Тераот

2
Дуже мало речей зламалося навіть під час переходу 95-> NT. Оригінальний SimCity для Windows все ще чудово працює у Windows 10 (32-розрядний). Навіть ігри DOS все ще працюють чудово, якщо ви відключите звук або використовуєте щось на зразок VDMSound, щоб консольна підсистема могла нормально працювати зі звуком. Microsoft сприймає зворотну сумісність дуже серйозно, і вони не приймають жодних ярликів "давайте помістимо її у віртуальну машину". Іноді це потребує вирішення, але це все ще досить вражає, особливо відносно.
Луань

11

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

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

Дійсно, справа зовсім не у вихідному коді. Більш абстрактним і релевантним твердженням OCP є: "компонент повинен допускати розширення, не порушуючи його меж абстракції". Я б хотів піти далі і сказати, що більш сучасним виданням є: "компонент повинен виконувати свої межі абстракції, але допускати розширення". Навіть у статті про OCP Боб Мартін, поки він "описує" "закритий до модифікації" як "вихідний код є непорушним", він пізніше починає говорити про інкапсуляцію, яка не має нічого спільного з зміною вихідного коду, і все, що стосується абстракції. межі.

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

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

Отже, щоб відповісти на ваші запитання: Ні, це не порушення OCP. Жодна зміна, яку автор вносить, не може бути порушенням OCP, оскільки OCP не є змістом. Зміни, однак, можуть створювати порушення OCP, і вони можуть бути мотивовані відмовами OCP у попередніх версіях бази даних коду. OCP - це властивість певного фрагмента коду, а не еволюційна історія бази даних.

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

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


3
"Ви, як творець, не порушуєте OCP, змінюючи або навіть видаляючи компонент." - Ви можете надати довідку для цього? Жодне з визначень принципу, який я бачив, не говорить про те, що "творець" (що б це не означало) не виключається з принципу. Видалення опублікованого компонента очевидно є надзвичайно важливою зміною.
ЖакБ

1
@JacquesB Люди і навіть зміни коду не порушують OCP, компоненти (тобто фактичні фрагменти коду). (І, щоб бути абсолютно зрозумілим, це означає, що компонент не може реагувати на саму OCP, а не на те, що він порушує OCP якогось іншого компонента.) Вся суть моєї відповіді полягає в тому, що OCP не говорить про зміни коду. , ламаючись чи іншим чином. Компонент або відкритий для розширення і закриті для модифікації, або це не так , так само , як метод може бути privateчи ні. Якщо автор згодом зробить privateметод public, це не означає, що вони порушили контроль доступу, (1/2)
Дерек Елкінс

2
... і це не означає, що метод не був насправді privateраніше. "Видалення опублікованого компонента очевидно є надзвичайною зміною", - це не послідовність. Або компоненти нової версії задовольняють OCP, або вони не відповідають, вам не потрібна історія кодової бази, щоб визначити це. За вашою логікою я ніколи не міг написати код, який задовольняє OCP. Ви поєднуєте сумісність назад, властивість коду змінюється, з OCP, властивістю коду. Ваш коментар має стільки ж сенсу, як сказати, що хитрощі не сумісні назад. (2/2)
Дерек Елкінс

3
@JacquesB По-перше, зауважте ще раз, що мова йде про модуль, що відповідає OCP. OCP - це порада щодо того, як написати модуль, щоб, зважаючи на обмеження, що вихідний код неможливо змінити, модуль все-таки можна розширити. Раніше в роботі він говорив про розробку модулів, які ніколи не змінюються, не про реалізацію процесу управління змінами, який ніколи не дозволяє модулям змінюватися. Посилаючись на редагування своєї відповіді, ви не "порушуєте OCP", змінюючи код модуля. Натомість, якщо "розширення" модуля вимагає від вас змінити вихідний код, (1/3)
Дерек Елкінс

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