¿Por qué es malo el "acoplamiento estrecho entre funciones y datos"?

38

Encontré esta cita en " La alegría de Clojure " en la pág. 32, pero alguien me dijo lo mismo durante la cena la semana pasada y también lo he escuchado en otros lugares:

[A] La desventaja de la programación orientada a objetos es el acoplamiento estrecho entre la función y los datos.

Entiendo por qué un acoplamiento innecesario es malo en una aplicación. También me siento cómodo diciendo que el estado mutable y la herencia deben evitarse, incluso en la programación orientada a objetos. Pero no veo por qué pegar funciones en las clases es inherentemente malo.

Quiero decir, agregar una función a una clase parece etiquetar un correo en Gmail o pegar un archivo en una carpeta. Es una técnica organizativa que te ayuda a encontrarla nuevamente. Eliges algunos criterios y luego juntas cosas similares. Antes de OOP, nuestros programas eran prácticamente bolsas de métodos en archivos. Quiero decir, tienes que poner funciones en alguna parte. ¿Por qué no organizarlos?

Si se trata de un ataque velado a los tipos, ¿por qué no dicen que restringir el tipo de entrada y salida a una función es incorrecto? No estoy seguro de si podría estar de acuerdo con eso, pero al menos estoy familiarizado con los argumentos pro y con seguridad de tipo. Esto me parece una preocupación mayoritariamente separada.

Claro, a veces las personas se equivocan y ponen la funcionalidad en la clase incorrecta. Pero en comparación con otros errores, esto parece un inconveniente muy menor.

Entonces, Clojure tiene espacios de nombres. ¿Cómo es diferente pegar una función en una clase en OOP de pegar una función en un espacio de nombres en Clojure y por qué es tan malo? Recuerde, las funciones en una clase no necesariamente operan solo en miembros de esa clase. Mire java.lang.StringBuilder: funciona en cualquier tipo de referencia, o mediante el auto-boxing, en cualquier tipo.

PD: Esta cita hace referencia a un libro que no he leído: Programación multiparadigm en Leda: Timothy Budd, 1995 .

GlenPeterson
fuente
20
Creo que el escritor simplemente no entendió OOP correctamente y solo necesitaba una razón más para decir que Java es malo y Clojure es bueno. / rant
Eufórico
66
Los métodos de instancia (a diferencia de las funciones gratuitas o los métodos de extensión) no se pueden agregar desde otros módulos. Esto se vuelve más una restricción cuando considera interfaces que solo pueden implementarse mediante los métodos de instancia. No puede definir una interfaz y una clase en diferentes módulos y luego usar el código de un tercer módulo para unirlos. Un enfoque más flexible, como las clases de tipos de Haskell, debería poder hacerlo.
CodesInChaos
44
@Euphoric Creo que el escritor lo entendió, pero a la comunidad de Clojure parece gustarle ser un hombre de paja y quemarlo como una efigie de todos los males de la programación antes de tener una buena recolección de basura, mucha memoria, procesadores rápidos y mucho espacio en disco Desearía que dejaran de golpear a OOP y apuntaran a las causas reales: la arquitectura de von Neuman, por ejemplo.
GlenPeterson
44
Mi impresión es que la mayoría de las críticas a OOP son en realidad críticas a OOP implementadas en Java. No porque sea un hombre de paja deliberado, sino porque es lo que asocian con OOP. Hay problemas bastante similares con las personas que se quejan de la escritura estática. La mayoría de los problemas no son inherentes al concepto, sino que solo fallan en una implementación popular de ese concepto.
CodesInChaos
3
Su título no coincide con el cuerpo de su pregunta. Es fácil explicar por qué el acoplamiento estrecho de funciones y datos es malo, pero su texto hace las preguntas "¿OOP hace esto?", "Si es así, ¿por qué?" y "¿Es esto algo malo?". Hasta ahora, ha tenido la suerte de recibir respuestas que abordan una o más de estas tres preguntas y ninguna asume la pregunta más simple en el título.
itsbruce

Respuestas:

34

En teoría, el acoplamiento flojo de datos y funciones hace que sea más fácil agregar más funciones para trabajar en los mismos datos. La desventaja es que hace que sea más difícil cambiar la estructura de datos en sí, por lo que en la práctica, el código funcional bien diseñado y el código OOP bien diseñado tienen niveles de acoplamiento muy similares.

Tome un gráfico acíclico dirigido (DAG) como una estructura de datos de ejemplo. En la programación funcional, aún necesita un poco de abstracción para evitar repetirse, por lo que creará un módulo con funciones para agregar y eliminar nodos y bordes, buscar nodos accesibles desde un nodo determinado, crear una clasificación topológica, etc. Esas funciones están efectivamente acoplados a los datos, aunque el compilador no los imponga. Puede agregar un nodo de la manera difícil, pero ¿por qué querría hacerlo? La cohesión dentro de un módulo evita un acoplamiento apretado en todo el sistema.

Por el contrario, en el lado de OOP, cualquier función distinta de las operaciones básicas de DAG se realizará en clases separadas de "vista", con el objeto DAG pasado como parámetro. Es tan fácil agregar tantas vistas como desee que operen en los datos del DAG, creando el mismo nivel de desacoplamiento de datos de función que encontraría en el programa funcional. El compilador no le impedirá agrupar todo en una clase, pero sus colegas sí.

Cambiar los paradigmas de programación no cambia las mejores prácticas de abstracción, cohesión y acoplamiento, solo cambia las prácticas que el compilador le ayuda a aplicar. En la programación funcional, cuando se desea el acoplamiento de datos de función, se aplica mediante un acuerdo de caballeros en lugar del compilador. En OOP, la separación de la vista del modelo se impone por acuerdo de caballeros en lugar del compilador.

Karl Bielefeldt
fuente
13

En caso de que no lo supiera, tome esta idea: los conceptos de objetos y cierres son dos caras de la misma moneda. Dicho esto, ¿qué es un cierre? Toma variables o datos del alcance circundante y se une a ellos dentro de la función, o desde una perspectiva OO, efectivamente hace lo mismo cuando, por ejemplo, pasa algo a un constructor para que luego pueda usarlo pieza de datos en una función miembro de esa instancia. Pero sacar las cosas del ámbito circundante no es algo agradable: cuanto mayor sea el ámbito circundante, más malvado es hacer esto (aunque pragmáticamente, a menudo es necesario algo de maldad para hacer el trabajo). El uso de variables globales está llevando esto al extremo, donde las funciones en un programa están usando variables en el alcance del programa, realmente muy malvado. Existenbuenas descripciones en otra parte sobre por qué las variables globales son malas.

Si sigues las técnicas de OO, básicamente ya aceptas que cada módulo de tu programa tendrá un cierto nivel mínimo de maldad. Si adopta un enfoque funcional para la programación, apunta a un ideal en el que ningún módulo en su programa contendrá el mal de cierre, aunque aún puede tener algunos, pero será mucho menos que OO.

Esa es la desventaja de OO: alienta este tipo de maldad, el acoplamiento de datos para funcionar a través del cierre estándar (una especie de teoría de programación de ventanas rotas ).

El único lado positivo es que, si sabía que iba a utilizar muchos cierres para comenzar, OO al menos le proporciona un marco ideal para ayudarlo a organizar ese enfoque para que el programador promedio pueda entenderlo. En particular, las variables que se están cerrando son explícitas en el constructor en lugar de simplemente tomarse implícitamente en el cierre de una función. Los programas funcionales que usan muchos cierres son a menudo más crípticos que el programa OO equivalente, aunque no necesariamente menos elegantes :)

Benedicto
fuente
8
Cita del día: "a menudo se necesita algo de maldad para hacer el trabajo"
GlenPeterson
55
Realmente no has explicado por qué las cosas que llamas mal son malas; solo los estás llamando malvados. Explica por qué son malvados y podrías tener una respuesta a la pregunta del caballero.
Robert Harvey
2
Sin embargo, su último párrafo guarda la respuesta. Puede ser el único lado positivo, según usted, pero eso no es poca cosa. Nosotros, los llamados "programadores promedio", en realidad damos la bienvenida a una cierta cantidad de ceremonia, sin duda suficiente para hacernos saber qué demonios está pasando.
Robert Harvey
Si OO y cierres son sinónimos, ¿por qué tantos lenguajes OO no han podido proporcionar un soporte explícito para ellos? La página wiki de C2 que usted cita tiene aún más disputas (y menos consenso) de lo normal para ese sitio.
itsbruce
1
@itsbruce Se hacen en gran medida innecesarios. Las variables que se "cerrarían" se convertirían en variables de clase pasadas al objeto.
Izkata
7

Se trata de tipo de acoplamiento:

Una función integrada en un objeto para trabajar en ese objeto no se puede usar en otros tipos de objetos.

En Haskell, usted escribe funciones para trabajar contra clases de tipos , por lo que hay muchos tipos diferentes de objetos con los que cualquier función puede trabajar, siempre que sea un tipo de la clase en la que funciona esa función.

Las funciones independientes permiten tal desacoplamiento que no obtienes cuando te enfocas en escribir tus funciones para trabajar dentro del tipo A porque entonces no puedes usarlas si no tienes una instancia de tipo A, aunque la función podría de lo contrario, sea lo suficientemente general como para usarse en una instancia de tipo B o una instancia de tipo C.

Jimmy Hoffa
fuente
3
¿No es ese el objetivo de las interfaces? ¿Para proporcionar las cosas que permiten que el tipo B y el tipo C se vean iguales a su función, para que pueda funcionar en más de un tipo?
Random832
2
@ Random832 absolutamente, pero ¿por qué incrustar una función dentro de un tipo de datos si no funciona con ese tipo de datos? La respuesta: es la única razón para incrustar una función en un tipo de datos. No podría escribir más que clases estáticas y hacer que todas sus funciones no se preocupen por el tipo de datos en el que están encapsuladas para que estén completamente desacopladas de su tipo propietario, pero entonces, ¿por qué molestarse en ponerlas en un tipo? El enfoque funcional dice: no se moleste, escriba sus funciones para trabajar hacia interfaces de algún tipo, y luego no hay razón para encapsularlas con sus datos.
Jimmy Hoffa
Aún tiene que implementar las interfaces.
Random832
2
@ Random832 las interfaces son tipos de datos; no necesitan funciones encapsuladas en ellos. Con funciones gratuitas, todas las interfaces que deben exaltarse son los datos que ponen a disposición para que las funciones funcionen.
Jimmy Hoffa
2
@ Random832 para relacionarse con objetos del mundo real como es tan común en OO, piense en la interfaz de un libro: presenta información (datos), eso es todo. Tiene la función gratuita de pasar la página que funciona contra la clase de tipos que tienen páginas, esta función funciona contra todo tipo de libros, periódicos, esos carteles en K-Mart, tarjetas de felicitación, correo, cualquier cosa grapada en el esquina. Si implementó la página de inicio como miembro del libro, se perderá todas las cosas que podría usar para activar la página, ya que una función gratuita no está vinculada; solo arroja una PartyFoulException en la cerveza.
Jimmy Hoffa
4

En Java y encarnaciones similares de OOP, los métodos de instancia (a diferencia de las funciones libres o los métodos de extensión) no se pueden agregar desde otros módulos.

Esto se vuelve más una restricción cuando considera interfaces que solo pueden implementarse mediante los métodos de instancia. No puede definir una interfaz y una clase en diferentes módulos y luego usar el código de un tercer módulo para unirlos. Un enfoque más flexible, como las clases de tipos de Haskell, debería poder hacerlo.

CodesInChaos
fuente
Puedes hacerlo fácilmente en Scala. No estoy familiarizado con Go, pero AFAIK también puedes hacerlo allí. En Ruby, también es una práctica bastante común agregar métodos a los objetos después del hecho para que se ajusten a alguna interfaz. Lo que usted describe parece más bien un sistema de tipos mal diseñado que cualquier cosa remotamente relacionada con OO. Solo como un experimento mental: ¿cómo sería diferente su respuesta al hablar de tipos de datos abstractos en lugar de objetos? No creo que haga ninguna diferencia, lo que probaría que su argumento no está relacionado con OO.
Jörg W Mittag
1
@ JörgWMittag Creo que te referías a los tipos de datos algebraicos. Y CodesInChaos, Haskell desalienta muy explícitamente lo que sugiere. Se llama una instancia huérfana y emite advertencias en GHC.
Daniel Gratzer
3
@ JörgWMittag Mi impresión es que muchos de los que critican OOP critican la forma de OOP utilizada en Java y lenguajes similares con su rígida estructura de clases y enfoque en métodos de instancia. Mi impresión de esa cita es que critica el enfoque en los métodos de instancia y realmente no se aplica a otros sabores de OOP, como lo que usa el golang.
CodesInChaos
2
@CodesInChaos Entonces quizás aclarando esto como "OO basado en clase estática"
Daniel Gratzer
@jozefg: estoy hablando de tipos de datos abstractos. Ni siquiera veo cómo los tipos de datos algebraicos son remotamente relevantes para esta discusión.
Jörg W Mittag
3

La orientación a objetos es fundamentalmente sobre la abstracción de datos de procedimiento (o la abstracción de datos funcionales si elimina los efectos secundarios que son un problema ortogonal). En cierto sentido, el cálculo Lambda es el lenguaje orientado a objetos más antiguo y puro, ya que solo proporciona abstracción de datos funcionales (porque no tiene construcciones además de las funciones).

Solo las operaciones de un solo objeto pueden inspeccionar la representación de datos de ese objeto. Ni siquiera otros objetos del mismo tipo pueden hacer eso. (Esta es la principal diferencia entre la abstracción de datos orientada a objetos y los tipos de datos abstractos: con los ADT, los objetos del mismo tipo pueden inspeccionar la representación de datos de cada uno, solo se oculta la representación de objetos de otros tipos).

Lo que esto significa es que varios objetos del mismo tipo pueden tener diferentes representaciones de datos. Incluso el mismo objeto puede tener diferentes representaciones de datos en diferentes momentos. (Por ejemplo, en Scala, Mapsys Setcambian entre una matriz y un hash trie dependiendo del número de elementos porque para números muy pequeños la búsqueda lineal en una matriz es más rápida que la búsqueda logarítmica en un árbol de búsqueda debido a los factores constantes muy pequeños .)

Desde el exterior de un objeto, no debería, no puede conocer su representación de datos. Eso es lo opuesto al acoplamiento apretado.

Jörg W Mittag
fuente
Tengo clases en OOP que cambian las estructuras de datos internas según las circunstancias, por lo que las instancias de objetos de estas clases pueden usar representaciones de datos muy diferentes al mismo tiempo. ¿Ocultación de datos básicos y encapsulación, diría? Entonces, ¿en qué se diferencia Map in Scala de una clase Map implementada correctamente (ocultación y encapsulación de datos wrt) en un lenguaje OOP?
Marjan Venema
En su ejemplo, encapsular sus datos con funciones de acceso en una clase (y, por lo tanto, unir estrechamente esas funciones con esos datos) en realidad le permite acoplar libremente instancias de esa clase con el resto de su programa. Estás refutando el punto central de la cita, ¡muy agradable!
GlenPeterson
2

El acoplamiento apretado entre datos y funciones es malo porque desea poder cambiar cada uno independientemente del otro y el acoplamiento apretado lo hace difícil porque no puede cambiar uno sin conocimiento y posiblemente cambios en el otro.

Desea que se presenten diferentes datos a la función para que no requieran ningún cambio en la función y, de manera similar, desea poder realizar cambios en la función sin necesidad de ningún cambio en los datos en los que está operando para admitir esos cambios de función.

Michael Durrant
fuente
1
Sí, quiero eso. Pero mi experiencia es que cuando envía datos a una función no trivial que no fue diseñada explícitamente para manejar, esa función tiende a romperse. No solo me refiero a la seguridad de tipo, sino a cualquier condición de datos que no haya sido anticipada por el autor o autores de la función. Si la función es antigua y se usa con frecuencia, cualquier cambio que permita que fluyan nuevos datos es probable que la rompa para alguna forma antigua de datos que aún necesita funcionar. Si bien el desacoplamiento puede ser el ideal para funciones frente a datos, la realidad de ese desacoplamiento puede ser difícil y peligroso.
GlenPeterson