Objetos versus estructuras de datos

En el artículo, cuya traducción se propone a continuación, Robert Martin parece comenzar con pensamientos muy similares a los que se pueden ver en las discusiones de Yegor Bugaenko sobre ORM, pero otros sacan conclusiones. Personalmente, el enfoque de Yegor me impresiona, pero creo que Martin revela el tema con más detalle. Me parece que vale la pena conocer a todos los que han pensado qué lugar debería ocupar ORM y, en general, por qué necesitamos objetos en los que todos los campos estén abiertos. El artículo está escrito en el género "Diálogo", donde un programador más experimentado discute un problema con alguien que tiene menos experiencia.


¿Qué es una clase?

Una clase es una especificación de muchos objetos similares.


¿Qué es un objeto?

Un objeto es un conjunto de funciones que realizan acciones con datos encapsulados.


¿O es mejor decir que un objeto es un conjunto de funciones que realizan acciones con datos cuya existencia está implícita

En el sentido de "implicado"?


Una vez que el objeto tiene funciones, se puede suponer que también hay datos allí, pero no hay acceso directo a los datos y no son visibles desde el exterior.

¿No están los datos en el objeto?


Tal vez lo son, pero la regla que dice que deben estar allí no existe. Desde el punto de vista del usuario, un objeto no es más que un conjunto de funciones. Los datos con los que funcionan estas funciones deben existir, pero el usuario desconoce la posición de estos datos.

Bueno, digamos


Bueno, ¿qué es una estructura de datos?

Una estructura de datos es una colección de elementos relacionados.


O, en otras palabras, una estructura de datos es un conjunto de elementos con los que funcionan las funciones, cuya existencia está implícitamente implícita.

Vale, vale. Lo entiendo Las funciones que funcionan con estructuras de datos no están definidas dentro de estas estructuras, pero a partir de la existencia misma de una estructura de datos podemos concluir que debe haber algo que funcione con ellas.


Derecho ¿Y qué hay de estas dos definiciones?

En cierto sentido, son opuestos entre sí.


De verdad. Se complementan entre sí. Como una mano y un guante.
  • Un objeto es un conjunto de funciones que funcionan con elementos de datos cuya existencia está implícitamente implícita.
  • Una estructura de datos es un conjunto de elementos de datos con los que funcionan las funciones, cuya existencia está implícitamente implícita.

Wow! ¡Entonces resulta que los objetos y las estructuras de datos no son lo mismo!


Derecho Las estructuras de datos son DTO.

Y las tablas en las bases de datos tampoco son objetos, ¿verdad?


Es cierto de nuevo. Las bases de datos contienen estructuras de datos, no objetos.

Espera un momento ¿ORM no asigna las tablas de la base de datos a los objetos?


Por supuesto que no. No puede asignar tablas de bases de datos a objetos. Las tablas en la base de datos son estructuras de datos, no objetos.

Entonces, ¿qué hace ORM?


Transfieren datos de una estructura a otra.

¿Entonces no tienen nada que ver con los objetos?


Nada en absoluto Estrictamente hablando, algo como ORM en el sentido de la tecnología que asigna datos relacionales a objetos no existe, porque las tablas no pueden asignarse desde una base de datos a objetos.

Pero me dijeron que los ORM recogen objetos comerciales.


No, ORM recupera datos de una base de datos con la que trabajan los objetos comerciales

¿Pero estas estructuras de datos no caen en objetos comerciales?


Tal vez lo consiguen, o tal vez no. ORM no sabe nada sobre esto.

Pero la diferencia es puramente semántica.


No no Hay consecuencias de largo alcance.

Por ejemplo?


Por ejemplo, el diseño del esquema de la base de datos y el diseño de objetos comerciales. Los objetos comerciales definen el comportamiento empresarial. El esquema de la base de datos define la estructura de datos comerciales. Estas estructuras están limitadas por fuerzas muy diferentes. Una estructura de datos empresariales no es necesariamente la mejor estructura para el comportamiento empresarial.

Eeee Esto es incomprensible.


Piénsalo de esa manera. El esquema de datos no está diseñado para una sola aplicación, está destinado a usarse en toda la empresa. Por lo tanto, la estructura de datos es un compromiso entre varias aplicaciones diferentes.

Esto es entendible.


Bueno Ahora piense en cada aplicación individual. El modelo de objetos de cada aplicación describe cómo se estructura el comportamiento de la aplicación. Cada aplicación tendrá su propio modelo de objeto para que coincida mejor con el comportamiento de la aplicación.

Ahh, ya veo. Dado que el esquema de datos es un compromiso entre diferentes aplicaciones, el esquema no recaerá en el modelo de objeto de cada aplicación individual.


Derecho! Los objetos y las estructuras se limitan a diferentes cosas. Muy raramente encajan. La gente llama a esto la falta de coincidencia de la impedancia relacional del objeto.

Algo que recuerdo. Pero parece que la falta de coincidencia de impedancia se corrigió utilizando ORM.


Y ahora sabes que esto no es así. La falta de coincidencia de impedancia entre los objetos y las estructuras de datos es complementaria, no isomorfa.

Que?


Son opuestos, no algo similar.

Opuestos?


Sí, en un sentido muy interesante. Verá, los objetos y las estructuras de datos implican estructuras de control diametralmente opuestas.

Que?


Piense en un conjunto de clases que implementan algún tipo de interfaz común. Por ejemplo, imagine clases que representan figuras bidimensionales, en las que hay funciones para calcular el área y el perímetro de una figura.

¿Cuánto empujan las formas al código con los objetos en todos los ejemplos?


Veamos dos tipos diferentes de formas: Cuadrados y Círculos. Está claro que las funciones para calcular el área y el perímetro de estas clases utilizan diferentes estructuras de datos. También se entiende que estas operaciones se invocan mediante polimorfismo dinámico.

Reduzca la velocidad por favor, nada está claro.


Hay dos funciones diferentes para calcular el área, una para el cuadrado y la otra para el círculo. Cuando se llama a una función para calcular el área de un objeto en particular, es este objeto el que decide a qué función en particular llamar. Esto se llama polimorfismo dinámico.

Esta bien Por supuesto Un objeto sabe cómo se implementan sus métodos. Naturalmente


Ahora vamos a convertir estos objetos en estructuras de datos. Usamos sindicatos discriminados.

Discriminado qué?


Uniones discriminadas. Bueno, C ++, punteros, la palabra clave de unión, una bandera para determinar el tipo de estructura, Uniones discriminadas. En nuestro caso, estas son solo dos estructuras de datos diferentes. Uno para el cuadrado y otro para el círculo. El círculo tiene un punto central y un radio. Y un código de tipo a partir del cual se puede entender que es un círculo.

El campo con el código será enum?


Pues si. Y el cuadrado tendrá el punto superior izquierdo y la longitud del lado. Y también enumeración para indicar el tipo.

Esta bien Habrá dos estructuras con un código de tipo.


Derecho Ahora veamos la función para el área. Probablemente habrá un cambio, ¿verdad?

Bueno Por supuesto, para dos clases. La rama para el cuadrado y para el círculo. Y para el perímetro, también necesita un interruptor similar.


Y de nuevo, correcto. Ahora piense en estos dos escenarios. En un escenario con objetos, dos implementaciones de funciones para un área son independientes entre sí y pertenecen (en cierto sentido) directamente al tipo. La función para el área del cuadrado pertenece al cuadrado, y la función para determinar el área del círculo pertenece al círculo.

De acuerdo, entiendo a lo que estás conduciendo. En un escenario con estructuras de datos, ambas implementaciones de una función para un área están en la misma función, no "pertenecen" (sea lo que sea que esa palabra signifique) al tipo.


Más lejos es mejor. En el caso de los objetos, si necesita agregar el tipo Triángulo, ¿qué código se debe cambiar?

No cambies nada en absoluto. Simplemente crea una nueva clase Triangle. Aunque no, probablemente deba corregir el código que crea los objetos.


Derecho Entonces, al agregar un nuevo tipo, los cambios son insignificantes. Ahora suponga que necesita agregar una nueva función, por ejemplo, una función para determinar el centro.

Luego debe agregarlo a los tres tipos, Círculo, Cuadrado y Triángulo.


Bueno Resulta que agregar nuevas funciones es difícil, porque tienes que hacer cambios en cada clase.

Pero con las estructuras de datos, todo es diferente. Para agregar un Triángulo, debe cambiar cada función para agregar ramas para manejar el Triángulo en cada interruptor.


Derecho Es difícil agregar tipos; tienes que editar cada función.

Pero para agregar una función para el centro, no es necesario cambiar nada.


Sí. Agregar funciones es fácil.

Wow Resulta que estos dos enfoques son directamente opuestos.


Definitivamente si. Para resumir

  • Es difícil agregar nuevas funciones a las clases, debes hacer cambios en cada clase
  • Agregar nuevas funciones a las estructuras de datos es simple, solo necesita agregar una función, no necesita cambiar nada más
  • Agregar nuevos tipos a las clases es simple, solo necesita agregar una nueva clase
  • Es difícil agregar nuevos tipos para estructuras; necesita arreglar cada función

Si Opuestos. Opuestos en un sentido curioso. Es decir, si se sabe de antemano que es necesario agregar nuevas funciones, es conveniente utilizar estructuras de datos. Pero si sabe de antemano que tiene que agregar nuevos tipos, entonces necesita usar clases.


Buena observación! Pero hoy necesitamos pensar en una cosa más. Hay otro punto en el que las estructuras de datos y las clases son opuestas entre sí. Dependencias

Adicciones?


Sí, la dirección de las dependencias en el código fuente.

Ok, preguntaré. Cual es la diferencia


Veamos el caso de las estructuras. Cada función contiene un conmutador que selecciona la implementación deseada en función del código de tipo en la unión.

Si lo es. ¿Y qué?


Veamos la llamada de función para el área. El código de llamada depende de la función para el área, y la función para el área depende de cada implementación específica.

¿Y a qué te refieres cuando dices "depende"?


Imagine que cada implementación de una función para un área se asigna a una función separada. Es decir, habrá funciones circleArea, squareArea y triangleArea.

Bueno, resulta que en las ramas del conmutador simplemente habrá llamadas a estas funciones.


Imagine que estas funciones están en diferentes archivos.

Luego, en el archivo con el interruptor se importará o usará o incluirá para archivos con funciones.


Derecho Esta es una dependencia en el nivel del código fuente. Una fuente depende de otra fuente. ¿Cómo se dirige esta dependencia?

El código fuente con el interruptor depende del código fuente en el que se encuentran las implementaciones.


¿Qué pasa con el código que llama a la función para el área?

El código de llamada depende del código con el interruptor, que depende de todas las implementaciones.


Derecho En todas las fuentes, la flecha se dirige en la dirección de la llamada, desde el código de llamada hasta la implementación. Entonces, si desea hacer un pequeño cambio en estas implementaciones ...

Vale, vale, ya veo a qué te refieres. Un cambio en cualquiera de las implementaciones implicará la recompilación de todos los archivos con el interruptor, y esto conducirá al hecho de que todo lo que llame a este interruptor se recompilará, por ejemplo, en nuestro caso, la función para el área.


Si Al menos lo será para los idiomas que usan fechas de modificación de archivos para comprender lo que debe reconstruirse.

Y estos son generalmente todos los sistemas con tipeo estático, ¿verdad?


Sí, y algunos otros sistemas sin él.

Esto tiene que ser reconstruido mucho.


Y mucho para rehacerlo.

De acuerdo, pero en el caso de las clases, ¿es al revés?


Sí, porque el código que llama a la función para el área depende de la interfaz, y la implementación también depende de esta interfaz.

Ya veo El código para la clase Square importará o usará o incluirá un archivo con la interfaz Shape.


Derecho La flecha en los archivos de implementación apunta en la dirección opuesta a la llamada. Se dirige desde el código de implementación al código de llamada. Al menos este será el caso de los idiomas tipados estáticamente. Para los idiomas escritos dinámicamente, el código que llama a la función para el área no depende en absoluto de nada, porque la vinculación ocurre en tiempo de ejecución.

Si, esta bien. Es decir, si realiza cambios en una de las implementaciones ...


Solo es necesario reconstruir y reinstalar el código con estos cambios.

Esto se debe a que las dependencias se dirigen opuestas a la dirección de las llamadas.


Sí, lo llamamos inversión de dependencia.

Bien, resumámoslo todo. Las clases y las estructuras de datos se oponen entre sí en tres sentidos.


  • Las funciones están explícitamente en las clases, y solo puede adivinar la existencia de datos. Las estructuras de datos están presentes explícitamente en las estructuras de datos, y solo puede adivinar qué funciones están disponibles.
  • En el caso de las clases, agregar tipos es simple, pero agregar funciones es difícil. En el caso de estructuras, agregar funciones es fácil, pero agregar tipos es difícil.
  • Las estructuras de datos conducen a la compilación y redistribución del código de llamada. Las clases aíslan el código de llamada y no necesitan volver a compilarlo e implementarlo nuevamente.

Si, eso es correcto. Y esto debe ser tenido en cuenta por cada diseñador y arquitecto de software.

Source: https://habr.com/ru/post/477712/


All Articles