Чому погана зв'язок між функціями та даними погана?


38

Цю цитату я знайшов у « Радості Клоджура » на с. 32, але хтось сказав те ж саме мені під час вечері минулого тижня, і я чув це також і в інших місцях:

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

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

Я маю на увазі, додавання функції до класу здається тегом поштової пошти в Gmail або вклеювання файлу в папку. Це організаційна техніка, яка допомагає вам знайти її знову. Ви вибираєте кілька критеріїв, а потім складаєте речі. До OOP наші програми були досить великими пакетами методів у файлах. Я маю на увазі, ви повинні десь поставити функції. Чому б не організувати їх?

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

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

Отже, у Clojure є простори імен. Чим наклеювання функції на клас в OOP відрізняється від закріплення функції в просторі імен у Clojure і чому це так погано? Пам'ятайте, що функції класу не обов'язково функціонують лише на членах цього класу. Подивіться на java.lang.StringBuilder - він працює на будь-якому еталонному типі, або через автоматичний бокс, будь-який тип взагалі.

PS Ця цитата посилається на книгу, яку я не читав: Мультипарадигмальне програмування в Leda: Timothy Budd, 1995 .


20
Я вважаю, що письменник просто не зрозумів OOP належним чином і просто потребував ще однієї причини сказати, що Java погана, а Clojure - це добре. / rant
Ейфорія

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

4
@Euphoric Я вважаю, що письменник це зрозумів, але, схоже, громада Clojure любить зробити солом’яну людину з OOP і спалити її як чудо для всіх злих програм, перш ніж ми мали хороший збір сміття, багато пам’яті, швидкі процесори та багато місця на диску. Я б хотів, щоб вони кинули бити на OOP і націлилися на реальні причини: наприклад, архітектура Фон Неймана.
GlenPeterson

4
Моє враження, що більшість критики OOP - це фактично критика OOP, реалізована на Java. Не тому, що це свідомо солом’яна людина, а тому, що вони асоціюються з ООП. Є досить схожі проблеми з людьми, які скаржаться на статичне введення тексту. Більшість питань не притаманні концепції, а лише недолікам у популярній реалізації цієї концепції.
CodesInChaos

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

Відповіді:


34

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

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

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

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


13

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

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

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

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


8
Цитата дня: "Часто потрібне зло, щоб закінчити роботу"
GlenPeterson

5
Ви насправді не пояснили, чому те, що ви називаєте злом, є злим; ти просто називаєш їх злими. Поясніть, чому вони злі, і ви можете мати відповідь на питання джентльмена.
Роберт Харві

2
Ваш останній абзац зберігає відповідь. На вашу думку, це може бути єдиним плюсом, але це не мало. Нас так звані «середні програмісти» насправді вітають певну кількість церемоній, безумовно, достатньо, щоб ми могли знати, що, до біса, відбувається.
Роберт Харві

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

1
@itsbruce Вони робляться в основному непотрібними. Змінні, які замість цього були "закриті", стануть змінними класу, переданими в об'єкт.
Ізката

7

Йдеться про тип з'єднання:

Функція, вбудована в об'єкт для роботи над цим об'єктом, не може використовуватися для інших типів об'єктів.

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

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


3
Чи не в цьому вся суть інтерфейсів? Щоб забезпечити речі, які дозволяють типу B і типу C виглядати однаково як вашій функції, щоб вона могла працювати на декількох типах?
Випадково832

2
@ Random832 абсолютно, але навіщо вбудовувати функцію всередині типу даних, якщо не працювати з цим типом даних? Відповідь: Це єдина причина , щоб вбудувати функції в типі даних. Ви не можете писати нічого, крім статичних класів, і зробити так, щоб усі ваші функції не піклувались про тип даних, в який вони інкапсульовані, щоб зробити їх повністю відірваними від їх власного типу, але чому тоді навіть не турбуватися про їх розміщення? Функціональний підхід говорить: Не турбуйтеся, пишіть свої функції, щоб вони працювали над різними інтерфейсами, і тоді немає жодної причини інкапсулювати їх своїми даними.
Джиммі Хоффа

Вам все одно доведеться реалізувати інтерфейси.
Випадково832

2
@ Random832 інтерфейси - це типи даних; їм не потрібні функції, закладені в них. З вільними функціями всі інтерфейси потребують прославлення - це ті дані, які вони роблять доступними для функціонування.
Джиммі Хоффа

2
@ Random832 для відновлення до реальних об'єктів, як це часто зустрічається в OO, подумайте про інтерфейс книги: вона подає інформацію (дані), ось і все. У вас є безкоштовна функція зворотної сторінки, яка працює проти класів типів, на яких є сторінки, ця функція працює проти різного роду книг, газет, газет, шпинделів плаката на K-Mart, вітальних листівок, пошти, будь-якого зв'язаного разом у куточок. Якщо ви реалізували перегорожену сторінку як учасника книги, ви пропускаєте всі речі, на яких можна було скористатись поворотною сторінкою, як вільну функцію вона не пов'язана; він просто кидає PartyFoulException на пиво.
Джиммі Хоффа

4

У Java та подібних втіленнях OOP методи екземплярів (на відміну від вільних функцій або методів розширення) не можна додавати з інших модулів.

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


Це можна легко зробити в Scala. Я не знайомий з Go, але AFAIK ви також можете це зробити там. У Ruby досить поширена практика також додати методи до об'єктів після того, щоб вони відповідали якомусь інтерфейсу. Те, що ви описуєте, виглядає скоріше погано сконструйованою системою типів, ніж будь-що, навіть віддалено пов’язане з ОО. Як мисленнєвий експеримент: як би відрізнялася ваша відповідь, якщо говорити про абстрактні типи даних замість об'єктів? Я не вірю, що це мало б значення, що могло б довести, що ваш аргумент не пов'язаний з ОО.
Йорг W Міттаг

1
@ JörgWMittag Я думаю, ви мали на увазі алгебраїчні типи даних. І CodesInChaos, Haskell дуже явно відлякує те, що ви пропонуєте. Це називається осиротілим екземпляром і видає попередження на GHC.
jozefg

3
@ JörgWMittag Моє враження, що багато хто критикує OOP, критикують форму OOP, що використовується на Java та подібних мовах, з її жорсткою структурою класів та зосередженістю на методах екземплярів. Моє враження від цієї цитати полягає в тому, що вона критикує увагу на методах екземплярів і насправді не стосується інших ароматів OOP, як, наприклад, того, що використовує голанг.
CodesInChaos

2
@CodesInChaos Тоді, можливо, уточнити це як "статичний клас на основі ОО"
jozefg

@jozefg: Я кажу про абстрактні типи даних. Я навіть не бачу, наскільки алгебраїчні типи даних віддалено мають відношення до цієї дискусії.
Йорг W Міттаг

3

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

Тільки операції з одним об'єктом можуть перевіряти представлення даних цього об'єкта. Навіть інші об'єкти одного типу не можуть цього зробити. (Це головна відмінність об'єктно-орієнтованих абстракцій даних від абстрактних типів даних: за допомогою ADT об'єкти одного типу можуть перевіряти подання даних один одного, приховується лише представлення об'єктів інших типів.)

Це означає, що кілька об'єктів одного типу можуть мати різні подання даних. Навіть той самий об’єкт може мати різні подання даних у різний час. (Наприклад, у Scala, Maps та Sets перемикаються між масивом та хеш-трієм залежно від кількості елементів, оскільки для дуже малих чисел лінійний пошук у масиві швидший, ніж логарифмічний пошук у дереві пошуку через дуже малі постійні фактори .)

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


У мене є класи в OOP, які перемикають внутрішні структури даних залежно від обставин, тому об’єктні екземпляри цих класів можуть одночасно використовувати дуже різні представлення даних. Основні дані про приховування та інкапсуляцію? Тож чим відрізняється Map у Scala від правильно реалізованого (приховання та інкапсуляція даних Wrt) Клас карт на мові OOP?
Мар'ян Венема

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

2

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

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


1
Так, я хочу цього. Але мій досвід полягає в тому, що коли ви надсилаєте дані в нетривіальну функцію, з якою вона не була чітко розроблена для обробки, ця функція має тенденцію до порушення. Я не просто маю на увазі безпеку типу, а скоріше будь-який стан даних, який не передбачав автор (-ів) функції. Якщо функція стара і часто використовується, будь-яка зміна, яка дозволяє протікати через неї нові дані, швидше за все, порушить її для якоїсь старої форми даних, яка ще повинна працювати. Хоча розв'язка може бути ідеальною для функцій проти даних, реальність цього роз'єднання може бути важкою і небезпечною.
GlenPeterson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.