Повна незмінність та об'єктно-орієнтоване програмування


43

У більшості мов OOP об'єкти, як правило, змінюються з обмеженим набором винятків (наприклад, кортежі та рядки в python). У більшості функціональних мов дані незмінні.

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

Є мови, які намагаються одружитися з обома поняттями, наприклад скала, де у вас є (явно заявлені) змінні та незмінні дані (будь ласка, виправте мене, якщо я помиляюся, мої знання про шкалу більш ніж обмежені).

Моє запитання: чи повна (sic!) Незмінність - якщо жоден об'єкт не може мутувати, коли він створений - має сенс у контексті OOP?

Чи існують розробки або реалізації такої моделі?

По суті, чи є (повна) незмінність та протилежності ООП чи ортогональні?

Мотивація: в OOP ви зазвичай працюєте над даними, змінюючи (мутуючи) базову інформацію, зберігаючи посилання між цими об'єктами. Наприклад об’єкт класу Personз членом, що fatherпосилається на інший Personоб'єкт. Якщо ви зміните ім'я батька, це відразу буде видно дочірньому об’єкту без необхідності оновлення. Будучи непорушними, вам потрібно буде побудувати нові об’єкти і для батька, і для дитини. Але у вас буде набагато менше kerfuffle із спільними об'єктами, багатопотоковою передачею, GIL тощо.


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

4
Змінюваність не є властивістю мов OOP, таких як C # та Java, і не є незмінною. Ви визначаєте незмінність або незмінність, як ви пишете клас.
Роберт Харві

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

2
@MichaelT Питання не в тому, щоб зробити певні речі незмінними, а в тому, щоб зробити все непорушним.

Відповіді:


43

OOP і незмінність майже повністю ортогональні один одному. Однак імперативного програмування та незмінності немає.

OOP можна узагальнити за двома основними ознаками:

  • Інкапсуляція : Я не матиму доступу до вмісту об'єктів безпосередньо, а краще спілкуватимусь через певний інтерфейс («методи») з цим об’єктом. Цей інтерфейс може приховати від мене внутрішні дані. Технічно це специфічно для модульного програмування, а не для ООП. Доступ до даних через визначений інтерфейс приблизно еквівалентний абстрактному типу даних.

  • Динамічна відправка : Коли я викликаю метод на об'єкті, виконаний метод буде вирішений під час виконання. (Наприклад, на основі класу OOP, я можу викликати sizeметод на IListекземплярі, але виклик може бути вирішений до реалізації в LinkedListкласі). Динамічна відправка - це один із способів дозволити поліморфну ​​поведінку.

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

Імперативна програма складається з тверджень, які виконуються послідовно. Заява має такі побічні ефекти, як зміна стану програми. З незмінністю стан неможливо змінити (звичайно, новий стан можна створити). Тому імперативне програмування принципово несумісне з незмінністю.

Зараз трапляється так, що OOP історично завжди був пов'язаний з імперативним програмуванням (Simula базується на Algol), а всі основні мови OOP мають імперативні корені (C ++, Java, C #,… усі коріння в C). Це не означає, що сам OOP був би імперативним або змінним, це просто означає, що реалізація OOP цими мовами дозволяє змінювати.


2
Дуже дякую, особливо за визначення двох основних особливостей.
Гіперборей

Динамічна відправка не є основною особливістю ООП. Насправді це не є капсулювання (як ви вже зізналися).
Зупиніть шкодити Моніці

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

2
"Об'єкти - це сполучення методів і даних". Все, що вам потрібно, - це те, що не має нічого спільного з незмінністю.
Зупиніть шкодити Моніці

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

25

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

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

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

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


17

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

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

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

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


Дякую. Чи можете ви, будь ласка, детальніше розібратися з останнім пунктом ("очевидний" може бути трохи суб'єктивним).
Гіперборей

Уже зробив ....
Роберт Харві

1
@Hyperboreus Існує багато способів досягти поліморфізму. Підтипизація динамічної диспетчеризації, статичний спеціальний поліморфізм (ака. Функція перевантаження) та параметричний поліморфізм (ака-генерик) - найпоширеніші способи цього зробити, і всі способи мають свої сильні та слабкі сторони. Сучасні мови OOP поєднують усі ці три способи, тоді як Haskell в основному покладається на параметричний поліморфізм та спеціальний поліморфізм.
амон

3
@RobertHarvey Ви говорите, що вам потрібна зміна, тому що вам потрібно зробити цикл (інакше вам доведеться використовувати рекурсію). Перш ніж я почав використовувати Haskell два роки тому, я вважав, що мені потрібні також змінні змінні. Я просто кажу, що існують інші способи "циклу" (карта, складання, фільтрування тощо). Після того, як ви знімаєте циклічну роботу зі столу, навіщо ще вам потрібні змінні змінні?
Кімманон

1
@RobertHarvey Але саме в цьому полягає мова мов програмування: те, що вам піддається, а не те, що відбувається під кришкою. Останнє - це відповідальність компілятора чи інтерпретатора, а не розробника програми. В іншому випадку поверніться до асемблера.
Гіперборей

5

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

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

  1. Ніяке посилання на об'єкт ніколи не буде піддаватися впливу будь-якого, що може змінити стан, закладений в ньому.

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

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

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

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


4

У c # деякі типи незмінні, як рядок.

Це, мабуть, ще більше говорить про те, що вибір був продуманий.

Напевно, це дійсно продуктивність, що вимагає використання незмінних типів, якщо вам доведеться модифікувати цей тип у сто тисяч разів. Ось чому в цих випадках пропонується використовувати StringBuilderклас замість stringкласу.

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

Це також інтуїтивно, якщо врахувати, що для зміни лише однієї літери в рядку з 4000 символів вам потрібно скопіювати кожну табличку в іншій області ОЗУ.


6
Часто модифікуючи незмінні дані не потрібно катастрофічно повільно, як при повторному stringконкатенації. Практично для всіх випадків використання даних / використання може бути (часто вже створено) ефективна стійка структура. Більшість із них мають приблизно однакові показники, навіть якщо постійні фактори іноді гірші.

@delnan Я також думаю, що останній абзац відповіді стосується скоріше деталізації реалізації, ніж про (ім) змінності.
Гіперборей

@Hyperboreus: ти вважаєш, я повинен видалити цю частину? Але як може змінюватися рядок, якщо вона непорушна? Я маю на увазі .. на мою думку, але напевно я можу помилятися, це може бути основною причиною того, що об’єкт не є незмінним.
Поважний

1
@Revious Ні в якому разі. Залиште це, щоб він викликав дискусію та більш цікаві думки та точки зору.
Гіперборей

1
@Revious Так, читання було б повільнішим, але не таким повільним, як зміна string(традиційне уявлення). "Рядок" (у поданні, про який я говорю) після 1000 модифікацій був би таким же, як щойно створений рядок (вміст модуля); жодна корисна або широко використовувана стійка структура даних не погіршує якість після X операцій. Фрагментація пам’яті не є серйозною проблемою (так, у вас є багато виділень, але, але фрагментація - це не проблема в сучасних сміттєзбірниках)

0

Повна незмінність усього не має великого сенсу в ООП або в більшості інших парадигм з цього питання з однієї дуже великої причини:

Кожна корисна програма має побічні ефекти.

Програма, яка нічого не спричиняє, нічого не потребує. Ви можете навіть не запустити його, оскільки ефект буде ідентичним.

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

Це може мати багато сенсу обмежити змінність частин, які потрібно мати можливість змінювати. Але якщо абсолютно нічого не потрібно змінювати, то ви нічого не робите, чого варто зробити.


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

І майбутня держава замінює нинішню, як саме? У програмі OO цей стан десь є властивістю об'єкта. Заміна стану вимагає зміни об’єкта (або заміни його на інший, що вимагає зміни об’єктів, які посилаються на нього (або заміни його на інший, який ... е. Ви отримуєте бал)). Ви можете придумати якийсь тип монадійного злому, коли кожна дія закінчується створенням цілком нового додатку ... але навіть тоді поточний стан програми має бути десь записаний.
cHao

7
-1. Це неправильно. Ви плутаєте побічні ефекти з мутацією, і хоча їх часто лікують однаковими функціональними мовами, вони відрізняються. Кожна корисна програма має побічні ефекти; не кожна корисна програма має мутацію.
Майкл Шоу

@Michael: Коли мова заходить про OOP, мутація та побічні ефекти настільки переплетені, що їх реально не можна розділити. Якщо у вас немає мутації, тоді ви не можете мати побічні ефекти без величезної кількості хакерів.
cHao


-2

Я думаю, це залежить від того, чи є ваше визначення OOP тим, що він використовує стиль передачі повідомлень.

Чисті функції не повинні нічого мутувати, оскільки вони повертають значення, які ви можете зберігати в нових змінних.

var brandNewVariable = pureFunction(foo);

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

sameOldObject.changeMe(foo);

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

var brandNewVariable = nonMutatingObject.askMe(foo);

Але неможливо змішати стиль передачі повідомлення та незмінні об’єкти.

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