Código limpio y objetos híbridos y envidia de características

10

Así que recientemente realicé algunas refactorizaciones importantes en mi código. Una de las cosas principales que intenté hacer fue dividir mis clases en objetos de datos y objetos de trabajo. Esto se inspiró, entre otras cosas, en esta sección de Clean Code :

Híbridos

Esta confusión a veces conduce a desafortunadas estructuras de datos híbridos que son mitad objeto y mitad estructura de datos. Tienen funciones que hacen cosas importantes, y también tienen variables públicas o accesores públicos y mutadores que, a todos los efectos, hacen públicas las variables privadas, tentando a otras funciones externas a usar esas variables de la forma en que un programa procesal usaría un estructura de datos.

Dichos híbridos dificultan la adición de nuevas funciones, pero también dificultan la adición de nuevas estructuras de datos. Son lo peor de ambos mundos. Evita crearlos. Son indicativos de un diseño confuso cuyos autores no están seguros o, lo que es peor, ignoran, si necesitan protección contra funciones o tipos.

Recientemente estaba mirando el código de uno de mis objetos de trabajo (que sucede para implementar el Patrón de visitante ) y vi esto:

@Override
public void visit(MarketTrade trade) {
    this.data.handleTrade(trade);
    updateRun(trade);
}

private void updateRun(MarketTrade newTrade) {
    if(this.data.getLastAggressor() != newTrade.getAggressor()) {
        this.data.setRunLength(0);
        this.data.setLastAggressor(newTrade.getAggressor());
    }
    this.data.setRunLength(this.data.getRunLength() + newTrade.getLots());
}

Inmediatamente me dije a mí mismo "¡envidia de características! Esta lógica debería estar en la Dataclase, específicamente en el handleTrademétodo, handleTradey siempreupdateRun debería suceder juntos". Pero luego pensé "la clase de datos es solo una estructura de datos, si empiezo a hacer eso, ¡entonces se convertirá en un Objeto Híbrido!"public

¿Qué es mejor y por qué? ¿Cómo decides qué hacer?

durron597
fuente
2
¿Por qué los 'datos' tienen que ser una estructura de datos? Tiene un comportamiento claro. Tan solo apague todos los captadores y establecedores, para que ningún objeto pueda manipular el estado interno.
Cormac Mulhall

Respuestas:

9

El texto que citó tiene buenos consejos, aunque reemplazaré "estructuras de datos" por "registros", suponiendo que se entiende algo como estructuras. Los registros son simples agregaciones de datos. Si bien pueden ser mutables (y, por lo tanto, con estado en una mentalidad de programación funcional), no tienen ningún estado interno, no hay invariantes que tengan que protegerse. Es completamente válido agregar operaciones a un registro que facilite su uso.

Por ejemplo, podemos argumentar que un vector 3D es un registro tonto. Sin embargo, eso no debería evitar que agreguemos un método como add, que facilita la adición de vectores. Agregar comportamiento no convierte un registro (no tan tonto) en un híbrido.

Esta línea se cruza cuando la interfaz pública de un objeto nos permite romper la encapsulación: hay algunos elementos internos a los que podemos acceder directamente, lo que lleva al objeto a un estado no válido. Me parece que Datasí tiene estado, y que puede llevarse a un estado no válido:

  • Después de manejar una operación, el último agresor podría no actualizarse.
  • El último agresor puede actualizarse incluso cuando no se produjo un nuevo comercio.
  • La duración de la ejecución puede conservar su valor anterior incluso cuando se actualizó el agresor.
  • etc.

Si algún estado es válido para sus datos, entonces todo está bien con su código, y puede continuar. De lo contrario: la Dataclase es responsable de su propia coherencia de datos. Si manejar una operación siempre implica actualizar al agresor, este comportamiento debe ser parte de la Dataclase. Si cambiar el agresor implica establecer la longitud de la ejecución en cero, este comportamiento debe ser parte de la Dataclase. DataNunca fue un disco tonto. Ya lo convertiste en un híbrido al agregar setters públicos.

Hay un escenario en el que puede considerar relajar estas responsabilidades estrictas: si Dataes privado para su proyecto y, por lo tanto, no forma parte de ninguna interfaz pública, aún puede garantizar el uso adecuado de la clase. Sin embargo, esto coloca la responsabilidad de mantener la Datacoherencia en todo el código, en lugar de recopilarlos en una ubicación central.

Recientemente escribí una respuesta sobre la encapsulación , que profundiza en lo que es la encapsulación y cómo puede garantizarla.

amon
fuente
5

El hecho de que handleTrade()y updateRun()siempre sucedan juntos (y el segundo método está realmente en el visitante y llama a varios otros métodos en el objeto de datos) huele a acoplamiento temporal . Esto significa que debe llamar a los métodos en un orden específico, y supongo que llamar a los métodos fuera de servicio romperá algo en el peor de los casos, o no proporcionará un resultado significativo en el mejor de los casos. No está bien.

Por lo general, la forma correcta de refactorizar esa dependencia es que cada método devuelva un resultado que se puede alimentar al siguiente método o actuar directamente.

Código antiguo

MyObject x = ...;
x.actionOne();
x.actionTwo();
String result = x.actionThree();

Nuevo código:

MyObject x = ...;
OneResult r1 = x.actionOne();
TwoResult r2 = r1.actionTwo();
String result = r2.actionThree();

Esto tiene varias ventajas:

  • Mueve preocupaciones separadas a objetos separados ( SRP ).
  • Elimina el acoplamiento temporal: es imposible llamar a los métodos fuera de servicio, y las firmas de métodos proporcionan documentación implícita sobre cómo llamarlos. ¿Alguna vez ha mirado la documentación, visto el objeto que desea y ha trabajado al revés? Quiero el objeto Z. Pero necesito una Y para obtener una Z. Para obtener una Y, necesito una X. ¡Ajá! Tengo una W, que se requiere para obtener una X. Encadena todo junto, y tu W ahora se puede usar para obtener una Z.
  • Dividir objetos como este es más probable que los haga inmutables, lo que tiene numerosas ventajas más allá del alcance de esta pregunta. La conclusión rápida es que los objetos inmutables tienden a generar código más seguro.

fuente
No hay acoplamiento temporal entre esas dos llamadas a métodos. Cambia su orden y el comportamiento no cambia.
durron597
1
Inicialmente también pensé en el acoplamiento secuencial / temporal al leer la pregunta, pero luego noté que el updateRunmétodo era privado . Evitar el acoplamiento secuencial es un buen consejo, pero solo se aplica al diseño de API / interfaces públicas, y no a los detalles de implementación. La verdadera pregunta parece ser si updateRundebería estar en el visitante o en la clase de datos, y no veo cómo esta respuesta aborda ese problema.
amon
La visibilidad de updateRunes irrelevante, lo importante es la implementación de la this.datacual no está presente en la pregunta y es el objeto manipulado por el objeto visitante.
En todo caso, el hecho de que este visitante simplemente llame a un grupo de emisores y no procese nada es una razón para que el acoplamiento temporal no esté presente. Probablemente no importa en qué orden se llamen los setters.
0

Desde mi punto de vista, una clase debe contener "valores de estado (variables miembro) e implementaciones de comportamiento (funciones miembro, métodos)".

Las "desafortunadas estructuras de datos híbridas" surgen si hace que las variables miembro de estado de clase (o sus captadores / establecedores) sean públicas y no deberían ser públicas.

Por lo tanto, veo que no es necesario tener clases separadas para objetos de datos de datos y objetos de trabajo.

Debería poder mantener las variables miembro-estado no públicas (su capa de base de datos debería poder manejar las variables miembro no públicas)

Una envidia de características es una clase que usa métodos de otra clase en exceso. Ver Code_smell . Tener una clase con métodos y estado eliminaría esto.

k3b
fuente