Чому класи не повинні бути розроблені як "відкриті"?


44

Під час читання різних запитань щодо переповнення стека та інших кодів загальний консенсус щодо способів проектування класів закритий. Це означає, що за замовчуванням у Java та C # все приватне, поля остаточні, деякі методи остаточні, а іноді класи навіть кінцеві .

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

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

  • Не розширюйте клас, просто вирішіть проблему, що призводить до більш складного коду
  • Скопіюйте та вставте весь клас, знищивши повторне використання коду
  • Розкладіть проект

Це не гарні варіанти. Чому не protectedвикористовується в проектах, написаних мовами, які його підтримують? Чому деякі проекти прямо забороняють успадковувати свої класи?


1
Я згоден, у мене була проблема з компонентами Java Swing, це дуже погано.
Йонас


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

2
"більшість мов OOP?" Я знаю набагато більше, де заняття не можна закрити. Ви залишили четвертий варіант: змінити мови.
кевін клайн

@Jonas Однією з найважливіших проблем компанії Swing є те, що вона створює занадто велику реалізацію. Це справді вбиває розвиток.
Том Хотін - тайклін

Відповіді:


55

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

Тож не так багато, що заняття не повинні бути відкритими, а те, що іноді не варто докладати зусиль, щоб зробити їх відкритими.

Звичайно, можливо також, що автори бібліотеки просто лінувалися. Залежить, про яку бібліотеку ви говорите. :-)


13
Так, саме тому класи запечатані за замовчуванням. Оскільки ви не можете передбачити, яким чином ваші клієнти можуть розширити клас, безпечніше його запечатати, ніж взяти на себе підтримку потенційно необмеженого числа способів розширення класу.
Роберт Харві


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

5
@TheLQ: Це досить велике припущення.
Роберт Харві

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

24

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

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

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


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

5
+1 за переваги сипучої муфти. @TheLQ, це інтерфейс, від якого слід сильно залежати, а не реалізація.
Карл Білефельдт

19

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

Щодо "остаточного" респ. "герметичні" класи, одним типовим випадком є ​​непорушні класи. Багато частин основи залежать від того, що струни незмінні. Якщо клас String не був остаточним, було б легко (навіть спокусливо) створити змінний підклас String, який викликає всілякі помилки у багатьох інших класах при використанні замість непорушного класу String.


4
Це справжня відповідь. Інші відповіді стосуються важливих речей, але справжній релевантний біт полягає в наступному: все, що немає finalі / або privateзаблоковано на місці і ніколи не може бути змінено .
Конрад Рудольф

1
@Konrad: Доки розробники не вирішать все переробити та не бути сумісним назад.
JAB

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

14

В ОО є два способи додати функціональність до існуючого коду.

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

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

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


5
Відмінний момент з композицією проти спадкування.
Zsolt Török

+1, але мені ніколи не сподобався приклад спадкування "є форма". Прямокутник і коло можуть бути двома дуже різними речами. Мені більше подобається використовувати, квадрат - прямокутник, який обмежений. Прямокутники можна використовувати, як Фігури.
tylermac

@tylermac: справді. Справа «Квадрат / Прямокутник» насправді є одним із контрприкладів LSP. Напевно, я повинен почати думати про більш ефективний приклад.
knulp

3
Квадрат / Прямокутник є лише контрприкладом, якщо ви дозволите зміни.
starblue

11

Більшість відповідей вірні: класи повинні бути запечатані (остаточні, NotOverridable тощо), коли об’єкт не розроблений або не призначений для розширення.

Однак я б не вважав просто закрити все належним дизайном коду. "O" в SOLID - це "Принцип відкритого закриття", вказуючи, що клас повинен бути "закритим" для модифікації, але "відкритим" для розширення. Ідея полягає в тому, що у вас є код в об'єкті. Це працює просто чудово. Додавання функціональності не повинно вимагати відкриття цього коду та внесення хірургічних змін, які можуть порушити раніше працюючу поведінку. Натомість треба мати можливість використовувати спадщину чи ін'єкцію залежності, щоб "розширити" клас, щоб зробити додаткові речі.

Наприклад, клас "ConsoleWriter" може отримати текст і вивести його на консоль. Це робить це добре. Однак у певному випадку вам ТАКОЖ потрібен той самий вихід, записаний у файл. Відкриття коду ConsoleWriter, зміна його зовнішнього інтерфейсу для додавання параметра "WriteToFile" до основних функцій та розміщення додаткового коду для запису файлів поруч із кодом запису консолі взагалі вважається поганою справою.

Натомість ви можете зробити одне з двох: ви можете отримати від ConsoleWriter для формування ConsoleAndFileWriter і розширити метод Write (), щоб спочатку викликати базову реалізацію, а потім також записати у файл. Або ви можете витягти IWriter інтерфейсу з ConsoleWriter і повторно застосувати цей інтерфейс для створення двох нових класів; FileWriter і "MultiWriter", яким можна надати інші IWriters і "транслюватимуть" будь-який виклик, здійснений за його методом, всім даним авторам. Кого ви обираєте, залежить від того, що вам потрібно буде; просте виведення - це ж простіше, але якщо у вас є неясний прогноз, з часом потрібно надіслати повідомлення мережевому клієнтові, три файли, два названі труби та консоль, продовжуйте і не вдається витягти інтерфейс та створити "Y-адаптер"; це '

Тепер, якщо функція Write () ніколи не була оголошена віртуальною (або вона була запечатана), тепер у вас виникли проблеми, якщо ви не контролюєте вихідний код. Іноді навіть якщо ти це робиш. Це, як правило, погана позиція, і це відлякує користувачів API з закритим джерелом або обмеженим кодом, коли це не відбувається. Але є законна причина, чому Write () або весь клас ConsoleWriter запечатані; може бути конфіденційною інформацією в захищеному полі (переоформлена в свою чергу від базового класу для надання зазначеної інформації). Можливо, недостатня перевірка; коли ви пишете api, ви повинні вважати, що програмісти, які споживають ваш код, не розумніші або доброзичливіші, ніж середній "кінцевий користувач" кінцевої системи; таким чином ви ніколи не розчаруєтесь.


Влучне зауваження. Спадкування може бути добрим чи поганим, тому неправильно говорити, що кожен клас повинен бути запечатаний. Це дійсно залежить від проблеми.
knulp

2

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

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


7
BTW, OO та процедурні найбільш очевидно НЕ взаємовиключні. Ви не можете мати ОО без процедур. Також композиція - це стільки ж концепція ОО, скільки і розширення.
Майкл К

@Michael звичайно ні. Я заявив, що ви можете хотіти процедурної системи, тобто такої, яка не має понять OO. Я бачив, як люди використовують Java для написання чисто процедурного коду, і він не працює, якщо ви намагаєтесь і взаємодіяти з ним в якості OO.
Спенсер Ратбун

1
Це було б вирішено, якби у Java були функції першого класу. Мех.
Майкл К

Новим студентам важко навчити мови Java або DOTNet. Зазвичай я викладаю процедуру з процедурним паскалем або "рівнем С", а пізніше переходжу на ОО з об'єктним паскалом або "С ++". І залиште Java на потім. Навчання програмуванню за допомогою процедурних програм виглядає так, що одноосібний об'єкт працює добре.
umlcat

@Michael K: У OOP функція першого класу - це лише об'єкт із точно одним методом.
Джорджіо

2

Тому що вони не знають нічого кращого.

Оригінальні автори, ймовірно, чіпляються за нерозуміння принципів SOLID, що виникли в заплутаному та складному світі C ++.

Сподіваюсь, ви помітите, що у світах рубіну, пітона та перла не виникають проблеми, які, як стверджують відповіді, є причиною запечатування. Зауважимо, що його ортогональне для динамічного набору тексту. Модифікаторами доступу легко працювати на більшості (усіх?) Мов. Полі C ++ можна вилучити, перейшовши на інший тип (C ++ слабкіший). Java та C # можуть використовувати відображення. Модифікатори доступу роблять речі досить важкими, щоб не допустити цього, якщо ви дійсно цього не хочете.

Опечатування класів та позначення будь-яких членів приватними членами явно порушує принцип, що прості речі повинні бути простими, а важкі речі повинні бути можливими. Раптом речі, які повинні бути простими, - це не так.

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

  1. Зарозумілість - вони дійсно вірили, що вони відкриті для розширення та закриті для модифікації
  2. Компенсація - вони знали, що можуть бути й інші випадки використання, але вирішили не писати для цих випадків використання

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

Вся ідея модифікаторів доступу полягає в тому, що програмісти повинні бути захищені від себе. "Програмування на С погано, тому що дозволяє стріляти в ногу". З філософією я не згоден як програміст.

Я дуже вважаю за краще керувати підходом до імені пітона. Ви можете легко (набагато простіше, ніж роздуми) замінити приватних, якщо вам потрібно. Відмінна реєстрація на ньому доступна тут: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Приватний модифікатор Ruby насправді більше схожий на захищений в C # і не має приватного модифікатора як C #. Захищений трохи інакше. Тут є чудове пояснення: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Пам'ятайте, що ваша статична мова не повинна відповідати старовинним стилям коду, написаного в цій мовній минувшині.


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

5
-1. Дурні кидаються туди, де ангели бояться ступати. Святкування здатності змінювати приватні змінні показує серйозний недолік у вашому стилі кодування.
riwalk

2
Окрім дискусійних та, мабуть, неправильних, ця публікація також є досить зарозумілою та образливою. Чудово зроблено.
Конрад Рудольф

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

1

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

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

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

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

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Класифікатори сфери застосування дозволяють «запечатати» деякі особливості, не обмежуючи спадкування.

Коли я перейшов з Structured Progr. ООП, я почав використовувати приватні член класу, але після багатьох проектів, я закінчив з використанням захищеним , і, в кінцевому рахунку, підвищити ті властивості і методи для громадськості , якщо це необхідно.

PS Мені не подобається "фінал" як ключове слово в C #, я скоріше використовую "запечатаний", як Java, або "стерильний", слідуючи метафорі успадкування. Крім того, "фінал" - це слово двозначне, яке вживається в декількох контекстах.


-1: Ви заявили, що любите відкриті конструкції, але це не дає відповіді на питання, через що класи не повинні бути розроблені для відкритих.
Джон

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