Почему плохая связь между функциями и данными плоха?

38

Я нашел эту цитату в « Радости Clojure » на с. 32, но кто-то сказал мне то же самое за ужином на прошлой неделе, и я слышал это и в других местах:

Недостатком объектно-ориентированного программирования является тесная связь между функцией и данными.

Я понимаю, почему ненужное соединение плохо в приложении. Также я могу с уверенностью сказать, что изменяемого состояния и наследования следует избегать даже в объектно-ориентированном программировании. Но я не понимаю, почему прикрепление функций к классам изначально плохо.

Я имею в виду, добавление функции к классу похоже на пометку почты в Gmail или наложение файла в папке. Это организационная техника, которая помогает вам снова ее найти. Вы выбираете некоторые критерии, а затем складываете все вместе. До ООП наши программы были довольно большими пакетами методов в файлах. Я имею в виду, вы должны поместить функции где-то. Почему бы не организовать их?

Если это скрытая атака на типы, почему бы им просто не сказать, что ограничение типа ввода и вывода функцией является неправильным? Я не уверен, смогу ли я согласиться с этим, но по крайней мере я знаком с аргументами за и против безопасности типа. Это звучит для меня как отдельная проблема.

Конечно, иногда люди ошибаются и ставят функциональность не на тот класс. Но по сравнению с другими ошибками это кажется очень незначительным неудобством.

Итак, у Clojure есть пространства имен. Чем наложение функции на класс в ООП отличается от наложения функции на пространство имен в Clojure и почему это так плохо? Помните, что функции в классе не обязательно работают только с членами этого класса. Посмотрите на java.lang.StringBuilder - он работает с любым ссылочным типом или через автобокс с любым типом вообще.

PS Эта цитата ссылается на книгу, которую я не читал: Программирование мультипарадигмы в Леде: Тимоти Бадд, 1995 .

GlenPeterson
источник
20
Я считаю, что писатель просто не понимал ООП должным образом, и ему просто нужна была еще одна причина, чтобы сказать, что Java - это плохо, а Clojure - это хорошо. / rant
Euphoric
6
Методы экземпляра (в отличие от свободных функций или методов расширения) не могут быть добавлены из других модулей. Это становится большим ограничением, когда вы рассматриваете интерфейсы, которые могут быть реализованы только методами экземпляра. Вы не можете определить интерфейс и класс в разных модулях, а затем использовать код из третьего модуля, чтобы связать их вместе. Более гибкий подход, такой как классы типов haskell, должен быть в состоянии сделать это.
CodesInChaos
4
@Euphoric Я полагаю, что писатель все- таки понял, но сообщество Clojure, похоже, хочет сделать соломенного человека из ООП и сжечь его как образ для всего зла программирования, прежде чем у нас будет хорошая сборка мусора, много памяти, быстрые процессоры и много места на диске. Хотелось бы, чтобы они перестали биться в ООП и нацелены на реальные причины: например, на архитектуру фон Неймана.
ГленПетерсон
4
У меня сложилось впечатление, что большая часть критики ООП фактически является критикой ООП, реализованной в Java. Не потому, что это преднамеренный соломенный человек, а потому, что это то, что они связывают с ООП. Есть довольно похожие проблемы с людьми, жалующимися на статическую типизацию. Большинство проблем не присуще этой концепции, а лишь изъяны в популярной реализации этой концепции.
CodesInChaos
3
Ваш заголовок не соответствует основанию вашего вопроса. Легко объяснить, почему плохая связь функций и данных плоха, но ваш текст задает вопросы «ООП делает это?», «Если так, то почему?» и "Это плохо?" До сих пор вам посчастливилось получить ответы, которые касаются одного или нескольких из этих трех вопросов, и ни один из них не предполагает более простой вопрос в названии.
Брюс

Ответы:

34

Теоретически, слабая связь функций с данными облегчает добавление большего количества функций для работы с одними и теми же данными. Недостатком является то, что это затрудняет изменение самой структуры данных, поэтому на практике хорошо разработанный функциональный код и хорошо разработанный код ООП имеют очень похожие уровни связи.

Возьмем ориентированный ациклический граф (DAG) в качестве примера структуры данных. В функциональном программировании вам все еще нужна некоторая абстракция, чтобы избежать повторения, поэтому вы создадите модуль с функциями для добавления и удаления узлов и ребер, поиска узлов, достижимых из данного узла, создания топологической сортировки и т. Д. Эти функции эффективно тесно связаны с данными, даже если компилятор не применяет их. Вы можете добавить узел трудным путем, но зачем вам это нужно? Связность внутри одного модуля предотвращает жесткое сцепление всей системы.

Наоборот, на стороне ООП любые функции, кроме базовых операций DAG, будут выполняться в отдельных классах «представления», причем объект DAG передается в качестве параметра. Так же просто добавить столько представлений, сколько вы хотите, которые работают с данными DAG, создавая тот же уровень развязки данных функций, что и в функциональной программе. Компилятор не помешает вам собрать все в один класс, но ваши коллеги это сделают.

Изменение парадигм программирования не меняет лучшие практики абстракции, сплоченности и связывания, оно просто меняет то, какие практики компилятор помогает вам применять. В функциональном программировании, когда вы хотите связать функцию-данные, это обеспечивается джентльменским соглашением, а не компилятором. В ООП разделение вида модели обеспечивается джентльменским соглашением, а не компилятором.

Карл Билефельдт
источник
13

В случае, если вы не знали, что это уже принимается: понимание объектно-ориентированных и замыканий - это две стороны одной медали. Тем не менее, что такое закрытие? Он берет переменную (и) или данные из окружающей области видимости и привязывается к ней внутри функции, или с точки зрения OO вы фактически делаете то же самое, когда вы, например, передаете что-то в конструктор, чтобы позже вы могли использовать это часть данных в функции-члене этого экземпляра. Но брать вещи из окружающей области - не очень хорошая вещь - чем больше окружающая область, тем больше зла это делать (хотя с практической точки зрения, какое-то зло часто необходимо для выполнения работы). Использование глобальных переменных доводит это до крайности, когда функции в программе используют переменные в области действия программы - действительно очень плохо. Естьхорошие описания в другом месте о том, почему глобальные переменные являются злом.

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

Это недостаток ОО - он поощряет такого рода зло, соединение данных для функционирования через стандартное закрытие (своего рода теория программирования с разбитыми окнами ).

Единственным плюсом является то, что, если вы знали, что для начала будете использовать множество замыканий, ОО, по крайней мере, предоставляет вам идеологическую структуру, помогающую организовать такой подход, чтобы обычный программист мог его понять. В частности, закрываемые переменные явны в конструкторе, а не просто взяты неявно в замыкании функции. Функциональные программы, которые используют много замыканий, часто более загадочны, чем эквивалентные ОО-программы, хотя не обязательно менее элегантны :)

Бенедикт
источник
8
Цитата дня: «Чтобы совершить работу, часто нужно какое-то зло»
GlenPeterson,
5
Вы действительно не объяснили, почему то, что вы называете злом, является злом; ты просто называешь их злом. Объясните, почему они злые, и у вас может быть ответ на вопрос джентльмена.
Роберт Харви,
2
Ваш последний абзац сохраняет ответ, хотя. Это может быть единственная плюс сторона, по вашему мнению, но это не мелочь. Мы, так называемые «среднестатистические программисты», на самом деле приветствуем определенное количество церемоний, безусловно, достаточно, чтобы сообщить нам, что, черт возьми, происходит.
Роберт Харви,
Если ОО и замыкания являются синонимами, почему так много языков ОО не смогли предоставить им явную поддержку? Вики-страница C2, которую вы цитируете, имеет даже больше споров (и меньше консенсуса), чем обычно для этого сайта.
Брюс
1
@itsbruce Они сделаны в основном ненужными. Переменные, которые будут «закрыты», вместо этого становятся переменными класса, передаваемыми в объект.
Изката,
7

Это о типе сцепления:

Функция, встроенная в объект для работы с этим объектом, не может использоваться для других типов объектов.

В Haskell вы пишете функции для работы с классами типов - поэтому существует множество различных типов объектов, с которыми может работать любая данная функция, при условии, что это тип данного класса, с которым работает функция.

Отдельно стоящие функции позволяют такое разделение, которое вы не получите, когда сосредоточитесь на написании своих функций для работы внутри типа A, потому что тогда вы не сможете использовать их, если у вас нет экземпляра типа A, даже если функция может в противном случае достаточно универсальны для использования с экземпляром типа B или экземпляром типа C.

Джимми Хоффа
источник
3
Разве это не весь смысл интерфейсов? Предоставить вещи, которые позволяют типу B и типу C выглядеть одинаково для вашей функции, чтобы она могла работать более чем с одним типом?
Random832 25.09.13
2
@ Random832 абсолютно, но зачем вставлять функцию внутри типа данных, если не работать с этим типом данных? Ответ: это единственная причина для встраивания функции в тип данных. Вы могли бы написать только статические классы и сделать так, чтобы все ваши функции не заботились о типе данных, в который они инкапсулированы, чтобы полностью отделить их от своего собственного типа, но тогда зачем вообще их помещать в тип? Функциональный подход гласит: не беспокойтесь, пишите свои функции для работы с интерфейсами своего рода, и тогда нет причин инкапсулировать их в свои данные.
Джимми Хоффа
Вы все еще должны реализовать интерфейсы.
Random832,
2
@ Random832 интерфейсы являются типами данных; им не нужны функции, заключенные в них. При наличии свободных функций все интерфейсы, которые необходимо превознести, - это данные, которые они предоставляют для функций, с которыми они могут работать.
Джимми Хоффа
2
@ Random832 для связи с реальными объектами, как это часто бывает в ОО, подумайте об интерфейсе книги: он представляет информацию (данные), вот и все. У вас есть бесплатная функция turn-page, которая работает с классом типов, у которых есть страницы, эта функция работает со всеми видами книг, газет, шпинделей плакатов в K-Mart, поздравительными открытками, почтой, всем, что скреплено вместе в угол. Если вы реализовали свою страницу в качестве члена книги, вы упустили все, что вы могли бы использовать для этой страницы, поскольку она не является бесплатной функцией; это просто создает PartyFoulException для пива.
Джимми Хоффа
4

В Java и подобных воплощениях ООП методы экземпляра (в отличие от свободных функций или методов расширения) не могут быть добавлены из других модулей.

Это становится большим ограничением, когда вы рассматриваете интерфейсы, которые могут быть реализованы только методами экземпляра. Вы не можете определить интерфейс и класс в разных модулях, а затем использовать код из третьего модуля, чтобы связать их вместе. Более гибкий подход, такой как классы типов Haskell, должен быть в состоянии сделать это.

CodesInChaos
источник
Вы можете сделать это легко в Scala. Я не знаком с Go, но AFAIK вы можете сделать это там же. В Ruby также довольно распространенной практикой является добавление методов к объектам после факта, чтобы они соответствовали некоторому интерфейсу. То, что вы описываете, скорее похоже на плохо спроектированную систему типов, чем на что-либо, даже отдаленно связанное с ОО. Так же, как мысленный эксперимент: как ваш ответ будет отличаться, когда речь идет об абстрактных типах данных вместо объектов? Я не верю, что это будет иметь какое-либо значение, что доказывает, что ваш аргумент не имеет отношения к ОО.
Йорг Миттаг
1
@ JörgWMittag Я думаю, вы имели в виду алгебраические типы данных. И CodesInChaos, Haskell очень явно препятствует тому, что вы предлагаете. Он называется осиротевшим инстансом и выдает предупреждения на GHC.
Даниэль Гратцер
3
@ JörgWMittag У меня сложилось впечатление, что многие, кто критикует ООП, критикуют форму ООП, используемую в Java и подобных языках, с ее жесткой структурой классов и фокусом на методах экземпляров. Мое впечатление от этой цитаты состоит в том, что она критикует фокус на методы экземпляра и на самом деле не относится к другим разновидностям ООП, например к тому, что использует Голанг.
CodesInChaos,
2
@CodesInChaos Тогда, возможно, пояснив это как «ОО на основе статических классов»
Даниэль Гратцер,
@jozefg: я говорю об абстрактных типах данных. Я даже не понимаю, как алгебраические типы данных имеют отдаленное отношение к этому обсуждению.
Йорг Миттаг,
3

Ориентация на объект - это, в основном, процедурная абстракция данных (или функциональная абстракция данных, если убрать побочные эффекты, которые являются ортогональной проблемой). В некотором смысле, лямбда-исчисление является старейшим и наиболее чистым объектно-ориентированным языком, поскольку оно обеспечивает только абстракцию функциональных данных (поскольку у него нет никаких конструкций, кроме функций).

Только операции одного объекта могут проверять представление данных этого объекта. Даже другие объекты того же типа не могут этого сделать. (Это основное различие между объектно-ориентированной абстракцией данных и абстрактными типами данных: с помощью ADT объекты одного типа могут проверять представление данных друг друга, только представление объектов других типов скрыто.)

Это означает, что несколько объектов одного типа могут иметь разные представления данных. Даже один и тот же объект может иметь разные представления данных в разное время. (Например, в Scala Maps и Sets переключаются между массивом и хеш-кодом в зависимости от количества элементов, потому что для очень малых чисел линейный поиск в массиве быстрее, чем логарифмический поиск в дереве поиска из-за очень малых постоянных факторов .)

С внешней стороны объекта вы не должны, вы не можете знать его представление данных. Это противоположность жесткой связи.

Йорг Миттаг
источник
У меня есть классы в ООП, которые переключают внутренние структуры данных в зависимости от обстоятельств, поэтому экземпляры объектов этих классов могут одновременно использовать совершенно разные представления данных. Основные данные сокрытия и инкапсуляции я бы сказал? Итак, чем Map в Scala отличается от правильно реализованного (за исключением скрытия и инкапсуляции данных) класса Map на языке ООП?
Марьян Венема
В вашем примере инкапсуляция ваших данных с помощью функций доступа в классе (и, следовательно, тесная связь этих функций с этими данными) фактически позволяет вам свободно связывать экземпляры этого класса с остальной частью вашей программы. Вы опровергаете центральную точку цитаты - очень приятно!
ГленПетерсон
2

Тесная связь между данными и функциями плохая, потому что вы хотите иметь возможность менять каждую независимо от другой, а тесная связь делает это трудным, потому что вы не можете изменить одну, не зная и, возможно, не изменяя другую.

Вы хотите, чтобы различные данные, представляемые в функцию, не требовали каких-либо изменений в функции, и аналогичным образом вы хотите иметь возможность вносить изменения в функцию, не требуя каких-либо изменений в данных, с которыми она работает, для поддержки этих изменений функции.

Майкл Даррант
источник
1
Да, я этого хочу. Но мой опыт показывает, что когда вы отправляете данные в нетривиальную функцию, для которой она явно не предназначена для обработки, эта функция имеет тенденцию ломаться. Я имею в виду не просто безопасность типов, а скорее любое условие данных, которое не было предусмотрено автором (ами) функции. Если функция старая и часто используемая, любое изменение, которое позволяет новым данным проходить через нее, может нарушить ее для какой-то старой формы данных, которая все еще должна работать. Хотя разделение может быть идеальным для функций по сравнению с данными, реальность такого разделения может быть сложной и опасной.
ГленПетерсон