what the law does demeter language-agnostic design-patterns coupling law-of-demeter

language agnostic - the - ¿Cómo resolver las violaciones de la Ley de Demeter?



demeter''s law (10)

Un colega y yo diseñamos un sistema para nuestro cliente y, en nuestra opinión, creamos un diseño limpio y agradable. Pero estoy teniendo problemas con algún acoplamiento que hemos introducido. Podría tratar de crear un diseño de ejemplo que incluya los mismos problemas que nuestro diseño, pero si me lo perdonas, crearé un extracto de nuestro diseño para respaldar la pregunta.

Estamos desarrollando un sistema para el registro de ciertos tratamientos para pacientes. Para evitar tener un enlace roto a la imagen describiré el diagrama de clase UML conceptual como una definición de clase de estilo de CA.

class Discipline {} class ProtocolKind { Discipline; } class Protocol { ProtocolKind; ProtocolMedication; //1..* } class ProtocolMedication { Medicine; } class Medicine { AdministrationRoute; } class AdministrationRoute {}

Trataré de explicar un poco sobre el diseño, un protocolo es la plantilla para un nuevo tratamiento. Y un protocolo es de cierto tipo y tiene medicamentos que deben administrarse. Según el protocolo, la dosis puede diferir para el mismo medicamento (entre otras cosas), por lo que se almacena en la clase ProtocolMedication. AdministrationRoute es la forma en que se administra el medicamento y se crea / actualiza independientemente de la gestión del protocolo.

He encontrado los siguientes lugares que vamos a tener una violación de la Ley de Demeter:

Violaciones de la Ley de Demeter

Dentro de la BLL

Por ejemplo, dentro de la lógica comercial de ProtocolMedication, existen reglas que dependen de la propiedad AdministrationRoute.Soluble del medicamento. El código se convertiría

if (!Medicine.AdministrationRoute.Soluble) { //validate constrains on fields }

Dentro de los repositorios

Un método que enumeraría todos los protocolos en una cierta Disciplina se escribiría como:

public IQueryable<Protocol> ListQueryable(Discipline discipline) { return ListQueryable().Where(p => (p.Kind.Discipline.Id == discipline.Id)); // Entity Frameworks needs you to compare the Id... }

Dentro de la interfaz de usuario

Usamos ASP.NET (sin MVC) para la interfaz de nuestro sistema, en mi opinión esta capa tiene actualmente las peores violaciones. La vinculación de datos de una vista de cuadrícula (una columna que debe mostrar la Disciplina de un protocolo debe vincularse a Kind.Discipline.Name), que son cadenas, por lo que no hay errores de tiempo de compilación .

<asp:TemplateField HeaderText="Discipline" SortExpression="Kind.Discipline.Name"> <ItemTemplate> <%# Eval("Kind.Discipline.Name")%> </ItemTemplate> </asp:TemplateField>

Así que creo que la pregunta real podría ser, ¿cuándo estaría bien mirarla más como la Sugerencia de Demeter, y qué se puede hacer para resolver las violaciones de la Ley de Demeter?

Tengo algunas ideas de mí mismo, pero las publicaré como respuestas para que puedan ser comentadas y votadas por separado. (No estoy seguro de que esta sea la manera TAN de hacerlo, si no, borraré mis respuestas y las agregaré a la pregunta).


Creo que ayuda recordar la razón de ser de LoD. Es decir, si los detalles cambian en cadenas de relaciones, tu código podría romperse. Dado que las clases que tiene son abstracciones cercanas al dominio del problema , entonces las relaciones probablemente no cambiarán si el problema no cambia, por ejemplo, el Protocolo usa la Disciplina para realizar su trabajo, pero las abstracciones son de alto nivel y no es probable que cambio. Piense en la ocultación de información, y no es posible que un Protocolo ignore la existencia de Disciplinas, ¿verdad? Tal vez estoy fuera del entendimiento del modelo de dominio ...

Este vínculo entre Protocolo y Disciplina es diferente a los detalles de "implementación", como el orden de las listas, el formato de las estructuras de datos, etc. que podrían cambiar por motivos de rendimiento, por ejemplo. Es cierto que esta es un área algo gris.

Creo que si hicieras un modelo de dominio, verías más acoplamiento que lo que está en tu diagrama de clase C #. [Editar] Agregué lo que sospecho son relaciones en el dominio de su problema con líneas discontinuas en el siguiente diagrama:

Por otro lado, siempre puedes refactorizar tu código aplicando la metáfora Decir, no preguntar :

Es decir, debe tratar de decir a los objetos lo que quiere que hagan; no les haga preguntas sobre su estado, tome una decisión y luego dígales qué hacer.

Ya ha cambiado el primer problema (BLL) con su answer . (Otra forma de abstraer aún más el BLL sería con un motor de reglas).

Para refactorizar el segundo problema (repositorios), el código interno

p.Kind.Discipline.Id == discipline.Id

probablemente podría ser reemplazado por algún tipo de llamada .equals () utilizando una API estándar para colecciones (soy más un programador Java, por lo que no estoy seguro del equivalente preciso de C #). La idea es ocultar los detalles de cómo determinar una coincidencia.

Para refactorizar el tercer problema (dentro de la IU), tampoco estoy familiarizado con ASP.NET, pero si hay una manera de decirle a un objeto Kind que devuelva los nombres de las Disciplinas (en lugar de preguntarle los detalles como en Tipo). Discipline.Name), ese es el camino a seguir para respetar LoD.


El tercer problema es muy simple: Discipline.ToString() debería evaluar la propiedad Name esta forma solo se llama a Kind.Discipline


En cuanto al primer ejemplo con la propiedad "soluble", tengo algunas observaciones:

  1. ¿Qué es "AdministrationRoute" y por qué un desarrollador espera obtener la propiedad soluble de un medicamento? Los dos conceptos parecen completamente sin relación. Esto significa que el código no se comunica muy bien y quizás la descomposición de las clases que ya tiene podría mejorarse. Cambiar la descomposición podría llevarlo a ver otras soluciones para sus problemas.
  2. Soluble no es un miembro directo de la medicina por una razón. Si encuentra que tiene que acceder directamente, entonces quizás debería ser un miembro directo. Si se necesita una abstracción adicional, entonces devuelva esa abstracción adicional del medicamento (ya sea directamente o por poder o fachadas). Cualquier cosa que necesite la propiedad soluble puede funcionar en la abstracción, y podría usar la misma abstracción para muchos tipos adicionales, como sustratos o vitaminas.

En lugar de ir hasta el final y proporcionar getters / setters para cada miembro de cada objeto contenido, una alteración más simple que puede hacer que le ofrece cierta flexibilidad para futuros cambios es darles a los objetos métodos que devuelvan sus objetos contenidos.

Por ejemplo, en C ++:

class Medicine { public: AdministrationRoute()& getAdministrationRoute() const { return _adminRoute; } private: AdministrationRoute _adminRoute; };

Entonces

if (Medicine.AdministrationRoute.Soluble) ...

se convierte

if (Medicine.getAdministrationRoute().Soluble) ...

Esto le da la flexibilidad de cambiar getAdministrationRoute () en el futuro para, por ejemplo, obtener el Routing de Administración de una tabla DB a pedido.


Estaba luchando con el LoD al igual que muchos de ustedes hasta que vi la sesión The Clean Code Talks llamada:

"No busques cosas"

El video te ayuda a usar Dependency Injection mejor, que inherentemente puede solucionar los problemas con LoD. Al cambiar un poco el diseño, puede pasar muchos objetos o subtipos de nivel inferior al construir un objeto primario, evitando así que el padre tenga que recorrer la cadena de dependencia a través de los objetos secundarios.

En su ejemplo, debe pasar AdministrationRoute al constructor de ProtocolMedication. Tendría que rediseñar algunas cosas para que tuviera sentido, pero esa es la idea.

Habiendo dicho eso, siendo nuevo en el LoD y sin ningún experto, tendería a estar de acuerdo contigo y DrJokepu. Probablemente haya excepciones al LoD como la mayoría de las reglas, y es posible que no se aplique a su diseño.

[Llego unos años tarde, sé que esta respuesta probablemente no ayudará al creador, pero no es por eso que estoy publicando esto]


La solución tradicional a las violaciones de Demeter es "decir, no preguntar". En otras palabras, en función de su estado, debe decirle a un objeto administrado (cualquier objeto que tenga) que realice alguna acción, y decidirá si hace lo que le pide, dependiendo de su propio estado.

Como un ejemplo simple: mi código usa un marco de registro, y le digo a mi registrador que quiero dar salida a un mensaje de depuración. El registrador luego decide, en función de su configuración (tal vez la depuración no está habilitada para ello) si realmente envía o no el mensaje a sus dispositivos de salida. Una infracción de LoD en este caso sería para mi objeto preguntar al registrador si va a hacer algo con el mensaje. Al hacerlo, ahora he acoplado mi código al conocimiento del estado interno del registrador (y sí, escogí este ejemplo intencionalmente).

Sin embargo, el punto clave de este ejemplo es que el registrador implementa el comportamiento .

Donde creo que el LoD se rompe es cuando se trata de un objeto que representa datos , sin ningún comportamiento .

En cuyo caso, la OMI que atraviesa el gráfico del objeto no es diferente de aplicar una expresión XPath a un DOM. Y agregar métodos como "isThisMedicationWarranted ()" es un enfoque peor, porque ahora está distribuyendo reglas de negocios entre sus objetos, haciéndolos más difíciles de entender.


Mi comprensión de las consecuencias de la Ley de Demeter parece ser diferente a la de DrJokepu: cada vez que lo aplico al código orientado a objetos, resulta en una encapsulación y cohesión más estrictas, en lugar de la adición de captadores adicionales para contraer rutas en el código de procedimiento.

Wikipedia tiene la regla como

Más formalmente, la Ley de Demeter para funciones requiere que un método M de un objeto O solo invoque los métodos de los siguientes tipos de objetos:

  1. O sí mismo
  2. Parámetros de M
  3. cualquier objeto creado / instanciado dentro de M
  4. Objetos componentes directos de O

Si tiene un método que toma ''cocina'' como parámetro, Demeter dice que no puede inspeccionar los componentes de la cocina, no que solo puede inspeccionar los componentes inmediatos.

Escribir un montón de funciones solo para satisfacer la Ley de Demeter como esta

Kitchen.GetCeilingColour()

solo parece una pérdida total de tiempo para mí y en realidad es mi manera de hacer las cosas

Si un método fuera de Kitchen se pasa a una cocina, por estricta Demeter no puede invocar ningún método sobre el resultado de GetCeilingColour ().

Pero de cualquier manera, el punto es eliminar la dependencia de la estructura en lugar de mover la representación de la estructura de una secuencia de métodos encadenados al nombre del método. Hacer métodos como MoveTheLeftHindLegForward () en una clase Dog no hace nada para cumplir con Demeter. En su lugar, llame a dog.walk () y deje que el perro maneje sus propias piernas.

Por ejemplo, ¿qué pasa si los requisitos cambian y también necesitaré la altura del techo?

Refactorizaría el código para que esté trabajando con sala y techos:

interface RoomVisitor { void visitFloor (Floor floor) ... void visitCeiling (Ceiling ceiling) ... void visitWall (Wall wall ... } interface Room { accept (RoomVisitor visitor) ; } Kitchen.accept(RoomVisitor visitor) { visitor.visitCeiling(this.ceiling); ... }

O puede ir más allá y eliminar getters totalmente al pasar los parámetros del techo al método visitCeiling, pero a menudo introduce un acoplamiento frágil.

Aplicando esto al ejemplo médico, esperaría que un SolubleAdminstrationRoute pueda validar el medicamento, o al menos llamar al método de validación de la medicina para Solubilidad Administrable si hay información encapsulada en la clase del medicamento que se requiere para la validación.

Sin embargo, Demeter se aplica a sistemas OO, donde los datos se encapsulan dentro de los objetos que operan sobre los datos, en lugar del sistema del que usted habla, que tiene diferentes capas y datos que pasan entre las capas en estructuras mudas y navegables. No veo que Demeter se pueda aplicar necesariamente a tales sistemas tan fácilmente como a los monolíticos o basados ​​en mensajes. (En un sistema basado en mensajes, no puede navegar a nada que no esté en los gramos del mensaje, por lo que está atrapado con Demeter, le guste o no)


Para el BLL, mi idea era agregar una propiedad sobre Medicina de esta manera:

public Boolean IsSoluble { get { return AdministrationRoute.Soluble; } }

Que es lo que creo que se describe en los artículos sobre la Ley de Demeter. ¿Pero cuánto costará esto la clase?


Sé que voy a obtener downvoted para la aniquilación total, pero debo decir que me gusta la Ley de Demeter. Ciertamente, cosas como

dictionary["somekey"].headers[1].references[2]

son realmente feos, pero considera esto:

Kitchen.Ceiling.Coulour

No tengo nada en contra de esto. Escribir un montón de funciones solo para satisfacer la Ley de Demeter como esta

Kitchen.GetCeilingColour()

solo parece una pérdida total de tiempo para mí y en realidad es mi forma de hacer las cosas. Por ejemplo, ¿qué pasa si los requisitos cambian y también necesitaré la altura del techo? Con la Ley de Demeter, tendré que escribir otra función en Kitchen para poder obtener la altura del techo directamente, y al final tendré un montón de pequeñas funciones getter en todas partes, que es algo que consideraría un poco desordenado.

EDITAR: Permítanme reformular mi punto:

¿Este nivel de abstracción es tan importante que dedicaré tiempo a escribir 3-4-5 niveles de getters / setters? ¿Realmente hace el mantenimiento más fácil? ¿El usuario final gana algo? ¿Vale la pena mi tiempo? No lo creo.


Tendría que asumir que la lógica de negocios que requiere Soluble también requiere otras cosas. Si es así, ¿puede una parte de ella quedar encapsulada en Medicina de una manera significativa (más significativa que Medicine.isSoluble ())?

Otra posibilidad (probablemente una solución exagerada y no completa al mismo tiempo) sería presentar la regla comercial como un objeto propio y usar el patrón de despacho doble / Visitante:

RuleCompilator { lookAt(Protocol); lookAt(Medicine); lookAt(AdminstrationProcedure) } MyComplexRuleCompilator : RuleCompilator { lookaAt(Protocol) lookAt(AdminstrationProcedure) } Medicine { applyRuleCompilator(RuleCompilator c) { c.lookAt(this); AdministrationProtocol.applyRuleCompilator(c); } }