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 Data
clase, específicamente en el handleTrade
método, handleTrade
y 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?
Respuestas:
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
Data
sí tiene estado, y que puede llevarse a un estado no válido: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
Data
clase es responsable de su propia coherencia de datos. Si manejar una operación siempre implica actualizar al agresor, este comportamiento debe ser parte de laData
clase. Si cambiar el agresor implica establecer la longitud de la ejecución en cero, este comportamiento debe ser parte de laData
clase.Data
Nunca 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
Data
es 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 laData
coherencia 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.
fuente
El hecho de que
handleTrade()
yupdateRun()
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
Nuevo código:
Esto tiene varias ventajas:
fuente
updateRun
mé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 siupdateRun
debería estar en el visitante o en la clase de datos, y no veo cómo esta respuesta aborda ese problema.updateRun
es irrelevante, lo importante es la implementación de lathis.data
cual no está presente en la pregunta y es el objeto manipulado por el objeto visitante.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.
fuente