oop - real - ¿Cómo entender el panorama general en una aplicación acoplada suelta?
programas para programacion orientada a objetos (10)
Hemos estado desarrollando código usando acoplamiento flexible e inyección de dependencia.
Muchas clases de estilo de "servicio" tienen un constructor y un método que implementa una interfaz. Cada clase individual es muy fácil de entender de forma aislada.
Sin embargo, debido a la soltura del acoplamiento, mirar una clase no le dice nada acerca de las clases a su alrededor o de dónde se ajusta en la imagen más grande.
No es fácil saltar a los colaboradores que usan Eclipse porque tienes que ir a través de las interfaces. Si la interfaz es Runnable
, eso no ayuda a encontrar qué clase está realmente conectada. Realmente es necesario volver a la definición del contenedor DI e intentar resolver las cosas desde allí.
Aquí hay una línea de código de una clase de servicio inyectado de dependencia:
// myExpiryCutoffDateService was injected,
Date cutoff = myExpiryCutoffDateService.get();
Acoplamiento aquí es tan suelto como puede ser. La fecha de caducidad se implementa literalmente de cualquier manera.
Esto es lo que podría parecer en una aplicación más acoplada.
ExpiryDateService = new ExpiryDateService();
Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );
Desde la versión estrechamente acoplada, puedo inferir que la fecha de corte se determina de alguna manera a partir del instrumento de pago utilizando una conexión de base de datos.
Estoy descubriendo que el código del primer estilo es más difícil de entender que el código del segundo estilo.
Puede argumentar que al leer esta clase, no necesito saber cómo se descifra la fecha de corte. Eso es cierto, pero si me estoy limitando a un error o estoy resolviendo dónde debe introducirse una mejora, esa es información útil para saber.
¿Alguien más está experimentando este problema? ¿Qué soluciones tienes? ¿Es esto algo para adaptarse? ¿Hay alguna herramienta para permitir la visualización de la forma en que las clases están conectadas entre sí? ¿Debo hacer las clases más grandes o más parejas?
(He dejado deliberadamente esta pregunta como agnóstica de contenedor ya que estoy interesado en las respuestas para cualquier).
Sin embargo, debido a la soltura del acoplamiento, mirar una clase no le dice nada acerca de las clases a su alrededor o de dónde se ajusta en la imagen más grande.
Esto no es exacto. Para cada clase, usted sabe exactamente de qué tipo de objetos depende la clase, para poder proporcionar su funcionalidad en tiempo de ejecución.
Los conoces porque sabes qué objetos se espera sean inyectados.
Lo que no sabe es la clase concreta concreta que se inyectará en el tiempo de ejecución, que implementará la interfaz o clase base de la que usted sabe que su (s) clase (s) dependen.
Entonces, si quiere ver cuál es la clase real inyectada, solo tiene que mirar el archivo de configuración para esa clase para ver las clases concretas que se inyectan.
También podría usar las instalaciones provistas por su IDE.
Dado que se refiere a Eclipse, Spring tiene un complemento para él y también tiene una pestaña visual que muestra los beans que configura. ¿Lo revisaste? ¿No es lo que estás buscando?
También echa un vistazo a la misma discusión en Spring Forum
ACTUALIZAR:
Leyendo tu pregunta nuevamente, no creo que esta sea una pregunta real.
Quiero decir esto de la siguiente manera.
Como todas las cosas, loose coupling
no es una panacea y tiene sus propias desventajas per se.
La mayoría tiende a centrarse en los beneficios, pero como cualquier solución tiene sus desventajas.
Lo que haces en tu pregunta es describir una de sus principales desventajas, que es que de hecho no es fácil ver el panorama general, ya que tienes todo lo configurable y conectado por cualquier cosa .
También existen otros inconvenientes, como el que se puede quejar, por ejemplo, de que es más lento que las aplicaciones acopladas y sigue siendo cierto.
En cualquier caso, reiterando, lo que describes en tu pregunta no es un problema que hayas pisado y puedes encontrar una solución estándar (o cualquiera de esa manera).
Uno de los inconvenientes del acoplamiento flexible es que debe decidir si este costo es más alto de lo que realmente gana, como en cualquier transacción de decisión de diseño.
Es como preguntar:
Hola, estoy usando este patrón llamado Singleton
. ¡Funciona muy bien, pero no puedo crear objetos nuevos! ¿Cómo puedo solucionar este problema chicos?
Bueno, no puedes; pero si lo necesita, quizás Singleton no es para usted ...
Acabo de tener una discusión interna sobre esto y terminé escribiendo este artículo, que creo que es demasiado bueno para no compartirlo. Estoy copiando aquí (casi) sin editar, pero a pesar de que es parte de una discusión interna más grande, creo que la mayor parte puede ser independiente.
La discusión trata sobre la introducción de una interfaz personalizada llamada IPurchaseReceiptService
, y si debe reemplazarse o no con el uso de IObserver<T>
.
Bueno, no puedo decir que tenga fuertes puntos de datos sobre esto, solo son algunas de las teorías que estoy persiguiendo ... Sin embargo, mi teoría sobre la sobrecarga cognitiva en este momento es más o menos así: considere su servicio especial de IPurchaseReceiptService
:
public interface IPurchaseReceiptService
{
void SendReceipt(string transactionId, string userGuid);
}
Si lo mantenemos como la interfaz de encabezado que es actualmente, solo tiene ese único método SendReceipt
. Eso es genial.
Lo que no es tan genial es que tengas que encontrar un nombre para la interfaz y otro nombre para el método. Hay un poco de superposición entre los dos: la palabra Recibo aparece dos veces. IME, a veces esa superposición puede ser aún más pronunciada.
Además, el nombre de la interfaz es IPurchaseReceiptService
, que tampoco es particularmente útil. El sufijo del Servicio es esencialmente el nuevo Administrador , y es, IMO, un olor de diseño.
Además, no solo tenía que nombrar la interfaz y el método, sino que también debe nombrar la variable cuando la usa:
public EvoNotifyController(
ICreditCardService creditCardService,
IPurchaseReceiptService purchaseReceiptService,
EvoCipher cipher
)
En este punto, esencialmente ha dicho lo mismo tres veces. Esto es, según mi teoría, una carga cognitiva y un olor que el diseño podría y debería ser más simple.
Ahora, contraste esto con el uso de una interfaz conocida como IObserver<T>
:
public EvoNotifyController(
ICreditCardService creditCardService,
IObserver<TransactionInfo> purchaseReceiptService,
EvoCipher cipher
)
Esto le permite deshacerse de la burocracia y reducir el diseño en el corazón del asunto. Aún tiene nombres reveladores de intenciones: solo cambia el diseño de una sugerencia de rol de nombre de tipo a una sugerencia de rol de nombre de argumento .
Cuando se trata de la discusión sobre "desconexión", no me hago ilusiones de que el uso de IObserver<T>
mágicamente hará que este problema desaparezca, pero tengo otra teoría al respecto.
Mi teoría es que la razón por la que muchos programadores encuentran que la programación de interfaces es tan difícil es exactamente porque están acostumbrados a la característica Ir a definición de Visual Studio (dicho sea de paso, este es otro ejemplo de cómo las herramientas arruinan la mente ). Estos programadores están constantemente en un estado mental en el que necesitan saber qué hay al otro lado de la interfaz. ¿Por qué es esto? ¿Podría ser porque la abstracción es pobre?
Esto se relaciona con el RAP , porque si confirmas la creencia de los programadores de que existe una implementación única y particular detrás de cada interfaz, no es de extrañar que piensen que las interfaces solo están en el camino.
Sin embargo, si aplica el RAP, espero que, lentamente, los programadores aprendan que detrás de una interfaz particular, puede haber cualquier implementación de esa interfaz, y su código de cliente debe ser capaz de manejar cualquier implementación de esa interfaz sin cambiar la corrección de el sistema. Si esta teoría es válida, acabamos de introducir el Principio de sustitución de Liskov en una base de código sin asustar a nadie con conceptos que no entienden.
Algunas herramientas conocen los marcos DI y saben cómo resolver dependencias, lo que le permite navegar su código de forma natural. Pero cuando eso no está disponible, solo tiene que usar las funciones que su IDE proporciona lo mejor que pueda.
Uso Visual Studio y un framework hecho a medida, por lo que el problema que describes es mi vida. En Visual Studio, SHIFT + F12 es mi amigo. Muestra todas las referencias al símbolo debajo del cursor. Después de un tiempo, te acostumbras a la navegación necesariamente no lineal a través de tu código, y se vuelve una segunda naturaleza pensar en términos de "qué clase implementa esta interfaz" y "dónde está el sitio de inyección / configuración para poder ver qué clase se está utilizando para satisfacer esta dependencia de interfaz ".
También hay extensiones disponibles para VS que proporcionan mejoras de interfaz de usuario para ayudar con esto, como Productivity Power Tools . Por ejemplo, puede pasar el puntero del ratón sobre una interfaz, aparecerá un cuadro de información y puede hacer clic en "Implementado por" para ver todas las clases en su solución implementando esa interfaz. Puede hacer doble clic para saltar a la definición de cualquiera de esas clases. (Todavía utilizo MAYÚSCULAS + F12 de todos modos).
Dependiendo de cuántos desarrolladores estén trabajando en proyectos y si desea reutilizar algunas partes en diferentes proyectos, el acoplamiento flexible puede ayudarlo mucho. Si su equipo es grande y el proyecto necesita abarcar varios años, tener un acoplamiento flexible puede ayudar ya que el trabajo se puede asignar a diferentes grupos de desarrolladores más fácilmente. Uso Spring / Java con muchas DI y Eclipse ofrece algunos gráficos para mostrar las dependencias. Usar F3 para abrir la clase debajo del cursor ayuda mucho. Como se indicó en publicaciones anteriores, conocer los atajos para su herramienta lo ayudará.
Otra cosa a considerar es crear clases o envoltorios personalizados ya que son más fáciles de rastrear que las clases comunes que ya tienes (como Date).
Si usa varios módulos o capa de aplicación puede ser un desafío entender exactamente qué flujo de proyecto es, por lo que puede necesitar crear / usar alguna herramienta personalizada para ver cómo todo está relacionado entre sí. He creado this para mí y me ayudó a entender la estructura del proyecto más fácilmente.
En mi opinión, un código débilmente acoplado puede ayudarte mucho, pero estoy de acuerdo contigo acerca de la legibilidad del mismo. El verdadero problema es que el nombre de los métodos también debe transmitir información valiosa.
Ese es el principio de la Intención-Revelación de Interfaz según lo establecido por el Diseño Dirigido por el Dominio ( http://domaindrivendesign.org/node/113 ).
Puedes renombrar el método get :
// intention revealing name
Date cutoff = myExpiryCutoffDateService.calculateFromPayment();
Le sugiero que lea a fondo sobre los principios DDD y su código podría volverse mucho más legible y, por lo tanto, más manejable.
Estoy asombrado de que nadie haya escrito sobre la capacidad de prueba (en términos de pruebas unitarias, por supuesto) del código acoplado suelto y la no capacidad de prueba (en los mismos términos) del diseño estrechamente acoplado. No es pan comido qué diseño debe elegir. Hoy, con todos los marcos de Mock and Coverage, es obvio, bueno, al menos para mí.
A menos que no haga pruebas unitarias de su código o piense que lo hace, pero de hecho no ... Las pruebas en aislamiento apenas se pueden lograr con un acoplamiento firme.
¿Crees que tienes que navegar a través de todas las dependencias de tu IDE? ¡Olvídalo! Es la misma situación que en el caso de la compilación y el tiempo de ejecución. Casi ningún error se puede encontrar durante la compilación, no puede estar seguro de si funciona a menos que lo pruebe, lo que significa ejecutarlo. ¿Quieres saber qué hay detrás de la interfaz? Pon un punto de quiebre y ejecuta la maldita aplicación.
Amén.
... actualizado después del comentario ...
No estoy seguro de si va a servirle, pero en Eclipse hay algo llamado vista jerárquica. Le muestra todas las implementaciones de una interfaz dentro de su proyecto (no estoy seguro si el espacio de trabajo también). Puede navegar a la interfaz y presionar F4. Luego, le mostrará todas las clases concretas y abstractas que implementan la interfaz.
He encontrado que The Brain es útil en el desarrollo como una herramienta de mapeo de nodos. Si escribe algunos scripts para analizar su fuente en XML The Brain acepta, puede navegar por su sistema fácilmente.
La salsa secreta es poner guías en los comentarios de tu código sobre cada elemento que quieras rastrear, luego se puede hacer clic en los nodos en El cerebro para llevarte a esa guía en tu IDE.
Si bien no sé cómo responder esta pregunta en un solo párrafo, intenté responderla en una publicación de blog en su lugar: http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx
Para resumir, encuentro que los puntos más importantes son:
- Comprender una base de código débilmente acoplada requiere una mentalidad diferente . Si bien es más difícil "saltar a los colaboradores", también debería ser más o menos irrelevante.
- El acoplamiento flojo se trata de entender una parte sin entender el todo . Raramente deberías necesitar entender todo al mismo tiempo.
- Cuando se centra en un error, debe confiar en los rastros de pila en lugar de en la estructura estática del código para aprender sobre los colaboradores.
- Es responsabilidad de los desarrolladores escribir el código para asegurarse de que sea fácil de entender; no es responsabilidad del desarrollador leer el código.
Una cosa que me ayudó es colocar múltiples clases estrechamente relacionadas en el mismo archivo. Sé que esto va en contra del consejo general (de tener 1 clase por archivo) y generalmente estoy de acuerdo con esto, pero en la arquitectura de mi aplicación funciona muy bien. A continuación, intentaré explicar en qué caso es esto.
La arquitectura de mi capa de negocios está diseñada alrededor del concepto de comandos de negocios. Las clases de comando (DTO simple con solo datos y sin comportamiento) están definidas y para cada comando hay un ''controlador de comando'' que contiene la lógica de negocio para ejecutar este comando. Cada controlador de comandos implementa la interfaz genérica ICommandHandler<TCommand>
, donde TCommand es el comando comercial real.
Los consumidores toman una dependencia de ICommandHandler<TCommand>
y crean nuevas instancias de comando y usan el controlador inyectado para ejecutar esos comandos. Esto se ve así:
public class Consumer
{
private ICommandHandler<CustomerMovedCommand> handler;
public Consumer(ICommandHandler<CustomerMovedCommand> h)
{
this.handler = h;
}
public void MoveCustomer(int customerId, Address address)
{
var command = new CustomerMovedCommand();
command.CustomerId = customerId;
command.NewAddress = address;
this.handler.Handle(command);
}
}
Ahora los consumidores solo dependen de un ICommandHandler<TCommand>
y no tienen ninguna noción de la implementación real (como debería ser). Sin embargo, aunque el Consumer
debe saber nada sobre la implementación, durante el desarrollo I (como desarrollador) estoy muy interesado en la lógica empresarial real que se ejecuta, simplemente porque el desarrollo se realiza en sectores verticales; lo que significa que a menudo estoy trabajando tanto en la interfaz de usuario como en la lógica empresarial de una función simple. Esto significa que a menudo cambio de lógica de negocios a lógica de UI.
Entonces lo que hice fue poner el comando (en este ejemplo, CustomerMovedCommand
y la implementación de ICommandHandler<CustomerMovedCommand>
) en el mismo archivo, con el comando primero. Debido a que el comando en sí es concreto (ya que es un DTO no hay razón para abstraerlo) saltar a la clase es fácil (F12 en Visual Studio). Al colocar el controlador al lado del comando, saltar al comando significa también saltar a la lógica de negocios.
Por supuesto, esto solo funciona cuando está bien que el comando y el manejador vivan en el mismo conjunto. Cuando sus comandos deben implementarse por separado (por ejemplo, cuando los reutiliza en un escenario de cliente / servidor), esto no funcionará.
Por supuesto, esto es solo el 45% de mi capa de negocios. Sin embargo, otra gran paz (digamos 45%) son las consultas y están diseñadas de manera similar, utilizando una clase de consulta y un manejador de consultas. Estas dos clases también se colocan en el mismo archivo que, una vez más, me permite navegar rápidamente a la lógica de negocios.
Debido a que los comandos y las consultas son aproximadamente el 90% de mi capa de negocios, en la mayoría de los casos puedo moverme muy rápido de la capa de presentación a la capa de negocios e incluso navegar fácilmente dentro de la capa de negocios.
Debo decir que estos son los dos únicos casos en que coloco varias clases en el mismo archivo, pero hace que la navegación sea mucho más fácil.
Si desea obtener más información sobre cómo diseñé esto, he escrito dos artículos sobre esto:
Documentación!
Sí, nombraste el mayor inconveniente del código acoplado suelto. Y si probablemente ya se dio cuenta de que al final, valdrá la pena, es verdad que siempre será más largo encontrar "dónde" para hacer sus modificaciones, y es posible que tenga que abrir algunos archivos antes de encontrar "el lugar correcto". ..
Pero ahí es cuando algo realmente importante: la documentación. Es extraño que ninguna respuesta mencionara explícitamente eso, es un requisito MAYOR en todo desarrollo de gran tamaño.
Documentación API
Un APIDoc con una buena función de búsqueda. Que cada archivo y, casi todos, cada método tienen una descripción clara.
Documentación de "gran imagen"
Creo que es bueno tener una wiki que explique el panorama general. Bob ha hecho un sistema de proxy? ¿Cómo funciona? ¿Maneja la autenticación? ¿Qué tipo de componente lo usará? No es un tutorial completo, sino solo un lugar donde puedes leer 5 minutos, descubrir qué componentes están involucrados y cómo están vinculados entre sí.
Estoy de acuerdo con todos los puntos de la respuesta de Mark Seemann, pero cuando entres en un proyecto por primera vez, incluso si entiendes bien los principios antes del desacoplamiento, necesitarás muchas adivinanzas, o algún tipo de de ayuda para averiguar dónde implementar una característica específica que desea desarrollar.
... Otra vez: APIDoc y un pequeño desarrollador Wiki.