Acoplamiento suelto en diseño orientado a objetos

16

Estoy tratando de aprender GRASP y encontré esto explicado ( aquí en la página 3 ) sobre Acoplamiento bajo y me sorprendió mucho cuando encontré esto:

Considere el método addTrackpara una Albumclase, dos métodos posibles son:

addTrack( Track t )

y

addTrack( int no, String title, double duration )

¿Qué método reduce el acoplamiento? El segundo sí, ya que la clase que usa la clase Álbum no tiene que conocer una clase Track. En general, los parámetros de los métodos deben usar tipos base (int, char ...) y clases de los paquetes java. *.

Tiendo a estar de acuerdo con esto; Creo que addTrack(Track t)es mejor que addTrack(int no, String title, double duration)por varias razones:

  1. Siempre es mejor que un método tenga la menor cantidad de parámetros posible (de acuerdo con el Código de limpieza del tío Bob, ninguno o uno preferiblemente, 2 en algunos casos y 3 en casos especiales; más de 3 necesitan refactorización; estas son, por supuesto, recomendaciones, no reglas de acebo) .

  2. Si se addTracktrata de un método de una interfaz, y los requisitos necesitan que Trackse tenga más información (por ejemplo, año o género), entonces la interfaz debe cambiarse y el método debe admitir otro parámetro.

  3. La encapsulación está rota; si addTrackestá en una interfaz, entonces no debe conocer los elementos internos de Track.

  4. En realidad, está más acoplado en la segunda forma, con muchos parámetros. Supongamos que el noparámetro necesita ser cambiado de inta longporque hay más de MAX_INTpistas (o por cualquier razón); entonces, tanto el Trackmétodo como el método deben cambiarse, mientras que si el método fuera addTrack(Track track)solo el Track, se cambiaría.

Los 4 argumentos están realmente conectados entre sí, y algunos de ellos son consecuencias de otros.

¿Qué enfoque es mejor?

m3th0dman
fuente
2
¿Es este un documento elaborado por un profesor o formador? Según la URL del enlace que proporcionó, parece que fue para una clase, aunque no veo ningún crédito en el documento sobre quién lo creó. Si esto fuera parte de una clase, le sugiero que haga estas preguntas a la persona que proporcionó el documento. Por cierto, estoy de acuerdo con su razonamiento: me parece evidente que una clase de Álbum querría saber inherentemente sobre una clase de Pista.
Derek
Honestamente, cada vez que leo sobre "Mejores prácticas", ¡las tomo con un grano de sal!
AraK
@Derek Encontré el documento buscando en Google "ejemplo de patrones de comprensión"; No sé quién lo escribió, pero como era de una universidad, creo que es confiable. Estoy buscando un ejemplo basado en la información dada e ignorando la fuente.
m3th0dman
44
@ m3th0dman "pero como era de una universidad, creo que es confiable". Para mí, porque es de una universidad, lo considero poco confiable. No confío en alguien que no haya trabajado en proyectos de varios años hablando de las mejores prácticas en el desarrollo de software.
AraK
1
@AraK Confiable no significa incuestionable; y eso es lo que estoy haciendo aquí, cuestionándolo.
m3th0dman

Respuestas:

15

Bueno, sus primeros tres puntos son en realidad sobre otros principios además del acoplamiento. Siempre tiene que encontrar un equilibrio entre los principios de diseño a menudo conflictivos.

Su cuarto punto es sobre el acoplamiento, y estoy totalmente de acuerdo con usted. El acoplamiento se trata del flujo de datos entre módulos. El tipo de contenedor en el que fluyen los datos es en gran medida irrelevante. Pasar una duración como un doble en lugar de como un campo de a Trackno elimina la necesidad de pasarlo. Los módulos aún necesitan compartir la misma cantidad de datos y aún tienen la misma cantidad de acoplamiento.

Tampoco está considerando todo el acoplamiento en el sistema como un agregado. Si bien la introducción de una Trackclase ciertamente agrega otra dependencia entre dos módulos individuales, puede reducir significativamente el acoplamiento del sistema , que es la medida importante aquí.

Por ejemplo, considere un botón "Agregar a lista de reproducción" y un Playlistobjeto. Se Trackpodría considerar la introducción de un objeto para aumentar el acoplamiento si solo considera esos dos objetos. Ahora tiene tres clases interdependientes en lugar de dos. Sin embargo, esa no es la totalidad de su sistema. También debe importar la pista, reproducir la pista, mostrar la pista, etc. Agregar una clase más a esa mezcla es insignificante.

Ahora considere la necesidad de agregar soporte para reproducir pistas a través de la red en lugar de solo localmente. Solo necesita crear un NetworkTrackobjeto que se ajuste a la misma interfaz. Sin el Trackobjeto, tendría que crear funciones en todas partes como:

addNetworkTrack(int no, string title, double duration, URL location)

Eso efectivamente duplica su acoplamiento, lo que requiere que incluso los módulos que no se preocupan por las cosas específicas de la red aún lo sigan, para poder transmitirlo.

Su prueba de efecto dominó es buena para determinar su verdadera cantidad de acoplamiento. Lo que nos preocupa es limitar los lugares a los que afecta un cambio.

Karl Bielefeldt
fuente
1
+ El acoplamiento a primitivas sigue siendo acoplamiento, sin importar cómo se corte.
JustinC
+1 por mencionar la opción agregar una URL / efecto dominó.
user949300
44
+1 Una lectura interesante sobre esto también sería la discusión del Principio de Inversión de Dependencia en DIP en la naturaleza donde el uso de tipos primitivos se ve realmente como un "olor" de Obsesión Primitiva con Valor Objeto como la solución. A mí me parece que sería mejor pasar un objeto Track que un montón de tipos primitivos ... Y si quieres evitar la dependencia / acoplamiento con clases específicas, usa interfaces.
Marjan Venema
Respuesta aceptada debido a una buena explicación sobre la diferencia entre el acoplamiento total del sistema y el acoplamiento de módulos.
m3th0dman
10

Mi recomendación es:

Utilizar

addTrack( ITrack t )

pero asegúrese de que ITracksea ​​una interfaz y no una clase concreta.

Album no conoce los aspectos internos de los ITrackimplementadores. Solo está acoplado al contrato definido por el ITrack.

Creo que esta es la solución que genera la menor cantidad de acoplamiento.

Tulains Córdova
fuente
1
Creo que Track es solo un simple objeto de transferencia de datos / bean, donde solo tiene campos y captadores / establecedores sobre ellos; ¿Se requiere una interfaz en este caso?
m3th0dman
66
¿Necesario? Probablemente no. Sugerente, sí. El significado concreto de una pista puede y evolucionará, pero lo que la clase consumidora requiere de ella probablemente no lo hará.
JustinC
2
@ m3th0dman Siempre depende de abstracciones, no de concreciones. Eso se aplica independientemente de Trackser tonto o inteligente. TrackEs una concreción. ITrackLa interfaz es una abstracción. De esa manera, podrá tener diferentes tipos de pistas en el futuro, siempre que cumplan con ellas ITrack.
Tulains Córdova
44
Estoy de acuerdo con la idea, pero pierdo el prefijo 'I'. De Clean Code, por Robert Martin, página 24: "El I anterior, tan común en los tacos heredados de hoy, es una distracción en el mejor de los casos y demasiada información en el peor. No quiero que mis usuarios sepan que les estoy entregando un interfaz."
Benjamin Brumfield
1
@BenjaminBrumfield Tienes razón. Tampoco me gusta el prefijo, aunque lo dejaré en la respuesta para mayor claridad.
Tulains Córdova
4

Yo diría que el segundo método de ejemplo probablemente aumenta el acoplamiento, ya que lo más probable es crear instancias de un objeto Track y almacenarlo en el objeto Album actual. (Como se sugirió en mi comentario anterior, asumiría que es inherente que una clase de Álbum tenga el concepto de una clase de Pista en algún lugar dentro de ella).

El primer método de ejemplo supone que se está instanciando una Pista fuera de la clase Álbum, por lo que, como mínimo, podemos suponer que la instanciación de la clase Pista no está acoplada a la clase Álbum.

Si las mejores prácticas sugirieran que nunca tenemos una referencia de clase en una segunda clase, la totalidad de la programación orientada a objetos se descartaría.

Derek
fuente
No veo cómo tener una referencia implícita a otra clase lo hace más acoplado que tener una referencia explícita. De cualquier manera, las dos clases están acopladas. Creo que es mejor que el acoplamiento sea explícito, pero no creo que sea "más" acoplado de ninguna manera.
TMN
1
@ TMN, el acoplamiento adicional está en cómo implico que el segundo ejemplo probablemente terminaría creando internamente un nuevo objeto Track. La creación de instancias del objeto se está acoplando a un método que, de lo contrario, debería simplemente agregar un objeto Track a algún tipo de lista en el objeto Álbum (rompiendo el Principio de responsabilidad única). Si la forma en que se crea el Track alguna vez necesita ser cambiada, el método addTrack () también necesitaría ser cambiado. Esto no es así en el caso del primer ejemplo.
Derek
3

El acoplamiento es solo uno de los muchos aspectos a tratar de obtener en su código. Al reducir el acoplamiento, no necesariamente está mejorando su programa. En general, esta es una mejor práctica, pero en este caso particular, ¿por qué no deberíaTrack debería conocerse?

Al usar una Trackclase a la que pasar Album, está haciendo que su código sea más fácil de leer, pero lo más importante, como mencionó, está convirtiendo una lista estática de parámetros en un objeto dinámico. Eso finalmente hace que su interfaz sea mucho más dinámica.

Usted menciona que la encapsulación está rota, pero no lo está. Albumdebe conocer los aspectos internos de Track, y si no usó un objeto, Albumtendría que conocer todos y cada uno de los datos que se le pasan antes de que pueda utilizarlo de todos modos. La persona que llama también debe conocer los aspectos internos de Track, ya que debe construir un Trackobjeto, pero la persona que llama debe conocer esta información de todos modos si se pasa directamente al método. En otras palabras, si la ventaja de la encapsulación es no conocer el contenido de un objeto, no podría utilizarse en este caso, ya que Albumdebe hacer uso de Trackla información de la misma manera.

Donde no querrá usar Trackes si Trackcontiene lógica interna a la que no le gustaría que la persona que llama tenga acceso. En otras palabras, si Albumfuera una clase que un programador que usa tu biblioteca usaría, no querrías que él useTrack si la usa para decir, llame a un método para mantenerla en la base de datos. El verdadero problema con esto radica en el hecho de que la interfaz está enredada con el modelo.

Para solucionar el problema, necesitaría separarse Tracken sus componentes de interfaz y sus componentes lógicos, creando dos clases separadas. Para la persona que llama, se Trackconvierte en una clase ligera que está destinada a contener información y ofrecer optimizaciones menores (datos calculados y / o valores predeterminados). En el interior Album, usaría una clase nombrada TrackDAOpara realizar el trabajo pesado asociado con guardar la información de Trackla base de datos.

Por supuesto, esto es solo un ejemplo. Estoy seguro de que este no es su caso en absoluto, así que siéntase libre de usar sin Trackculpa. Solo recuerde tener en cuenta a la persona que llama cuando construye clases y crear interfaces cuando sea necesario.

Neil
fuente
3

Ambos son correctos

addTrack( Track t ) 

es mejor (como ya discutiste) mientras

addTrack( int no, String title, double duration ) 

está menos acoplado porque el código que usa addTrackno necesita saber que hay una Trackclase. Se puede cambiar el nombre de la pista, por ejemplo, sin la necesidad de actualizar el código de llamada.

Mientras habla de un código más legible / mantenible, el artículo habla de acoplamiento . Un código menos acoplado no es necesariamente más fácil de implementar y comprender.

k3b
fuente
Ver argumento 4; No veo cómo el segundo está menos acoplado.
m3th0dman
3

Acoplamiento bajo no significa sin acoplamiento. Algo, en algún lugar, debe saber acerca de los objetos en otras partes de la base de código, y cuanto más reduzca la dependencia de los objetos "personalizados", más razones dará para que cambie el código. Lo que el autor que cita está promoviendo con la segunda función está menos acoplado, pero también menos orientado a objetos, lo que es contrario a la idea de que GRASP sea una metodología de diseño orientada a objetos . El punto es cómo diseñar el sistema como una colección de objetos y sus interacciones; evitarlos es como enseñarle a conducir un automóvil diciéndole que debe andar en bicicleta.

En cambio, la vía adecuada es reducir la dependencia de los objetos concretos , que es la teoría del "acoplamiento flojo". Cuantos menos tipos concretos concretos tenga que conocer un método, mejor. Solo con esa declaración, la primera opción está en realidad menos acoplada, porque el segundo método que toma los tipos más simples debe conocer todos esos tipos más simples. Seguro que están incorporados, y el código dentro del método puede tener que importar, pero la firma del método y las personas que llaman definitivamente no lo hacen . Cambiar uno de estos parámetros relacionados con una pista de audio conceptual requerirá más cambios cuando están separados versus cuando están contenidos en un objeto de Pista (que es el punto de los objetos; encapsulación).

Yendo un paso más allá, si se espera que Track sea reemplazado por algo que hizo el mismo trabajo mejor, tal vez una interfaz que defina la funcionalidad requerida estaría en orden, un ITrack. Eso podría permitir implementaciones diferentes, como "AnalogTrack", "CdTrack" y "Mp3Track", que proporcionaban información adicional más específica para esos formatos, al tiempo que proporcionaban la exposición de datos básicos de ITrack que conceptualmente representa una "pista"; Una sub-pieza finita de audio. Track también podría ser una clase base abstracta, pero esto requiere que siempre desee utilizar la implementación inherente en Track; vuelva a implementarlo como BetterTrack y ahora debe cambiar los parámetros esperados.

Así la regla de oro; Los programas y sus componentes de código siempre tendrán motivos para cambiar. No puede escribir un programa que nunca requiera editar el código que ya ha escrito para agregar algo nuevo o modificar su comportamiento. Su objetivo, en cualquier metodología (GRASP, SOLID, cualquier otro acrónimo o palabra de moda que pueda imaginar) es simplemente identificar las cosas que tendrán que cambiar con el tiempo y diseñar el sistema para que esos cambios sean lo más fáciles de hacer posible (traducido; tocar la menor cantidad de líneas de código y afectar la menor cantidad posible de otras áreas del sistema más allá del alcance de su cambio previsto). Caso en cuestión, lo que es más probable que cambie es que un Track obtendrá más miembros de datos que a addTrack () le pueden importar o no, no esa pista se reemplazará con BetterTrack.

KeithS
fuente