El principio del menor conocimiento.

32

Entiendo el motivo detrás del principio de menor conocimiento , pero encuentro algunas desventajas si trato de aplicarlo en mi diseño.

Uno de los ejemplos de este principio (en realidad, cómo no usarlo), que encontré en el libro Head First Design Patterns especifica que es incorrecto llamar a un método en objetos que fueron devueltos de llamar a otros métodos, en términos de este principio .

Pero parece que a veces es muy necesario usar esa capacidad.

Por ejemplo: tengo varias clases: clase de captura de video, clase de codificador, clase de transmisor, y todas usan alguna otra clase básica, VideoFrame, y dado que interactúan entre sí, pueden hacer, por ejemplo, algo como esto:

streamer código de clase

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

Como puede ver, este principio no se aplica aquí. ¿Se puede aplicar este principio aquí, o es que este principio no siempre se puede aplicar en un diseño como este?

ransh
fuente
posible duplicado del método de encadenamiento vs encapsulación
mosquito
44
Probablemente sea un duplicado más cercano.
Robert Harvey
1
Relacionado: ¿Es un código como este un "choque de trenes" (en violación de la Ley de Demeter)? (divulgación completa: esa es mi propia pregunta)
un CVn

Respuestas:

21

El principio del que está hablando (mejor conocido como Ley de Demeter ) para las funciones se puede aplicar agregando otro método auxiliar a su clase streamer como

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

Ahora, cada función solo "habla con amigos", no con "amigos de amigos".

En mi humilde opinión, es una guía aproximada que puede ayudar a crear métodos que sigan más estrictamente el principio de responsabilidad única. En un caso simple como el anterior, probablemente sea muy testarudo si realmente vale la pena y si el código resultante es realmente "más limpio", o si simplemente expandirá su código formalmente sin ninguna ganancia notable.

Doc Brown
fuente
Este enfoque tiene la ventaja de que es mucho más fácil realizar pruebas unitarias, depurar y razonar sobre su código.
gntskn
+1. Sin embargo, hay una muy buena razón para no hacer que el cambio de código sea que la interfaz de la clase se puede inflar con métodos cuyo trabajo es simplemente realizar una o algunas llamadas a varios otros métodos (y sin lógica adicional). En algunos tipos de entornos de programación, por ejemplo, C ++ COM (especialmente WIC y DirectX), hay un alto costo asociado con la adición de cada método individual a una interfaz COM, en comparación con otros lenguajes.
rwong
1
En el diseño de la interfaz COM de C ++, descomponer clases grandes en clases pequeñas (lo que significa que tiene una mayor probabilidad de tener que hablar con varios objetos) y minimizar la cantidad de métodos de interfaz son dos objetivos de diseño con beneficios reales (reducción de costos) basados ​​en una comprensión profunda de la mecánica interna (tablas virtuales, reutilización de código, muchas otras cosas). Por lo tanto, los programadores COM de C ++ generalmente deben ignorar LoD.
rwong
10
+1: La Ley de Demeter realmente debería llamarse La sugerencia de Demeter: YMMV
Binary Worrier
La ley de demeter es un consejo para tomar decisiones arquitectónicas, mientras que usted sugiere un cambio cosmético, por lo que parece cumplir con esa 'ley'. Eso sería un error, porque un cambio sintáctico no significa que de repente todo esté bien. Hasta donde sé, la ley de demeter básicamente significaba: si quieres hacer OOP, entonces deja de escribir código prodecural con funciones getter en todas partes.
user2180613
39

El Principio de menor conocimiento o la Ley de Demeter es una advertencia contra el enredo de su clase con detalles de otras clases que atraviesan capa tras capa. Le dice que es mejor hablar solo con sus "amigos" y no con "amigos de amigos".

Imagine que le han pedido que suelde un escudo sobre una estatua de un caballero con una armadura de placas brillantes. Coloca cuidadosamente el escudo en el brazo izquierdo para que se vea natural. Notará que hay tres lugares pequeños en el antebrazo, el codo y la parte superior del brazo donde el escudo toca la armadura. Sueldes los tres lugares porque quieres asegurarte de que la conexión sea fuerte. Ahora imagine que su jefe está enojado porque no puede mover el codo de su armadura. Asumiste que la armadura nunca se movería y creaste una conexión inmóvil entre el antebrazo y la parte superior del brazo. El escudo solo debe conectarse a su amigo, antebrazo. No a los antebrazos amigos. Incluso si tiene que agregar un trozo de metal para que se toquen.

Las metáforas son buenas, pero ¿qué queremos decir con amigo? Cualquier cosa que un objeto sepa cómo crear o encontrar es un amigo. Alternativamente, un objeto puede pedir que le entreguen otros objetos , de los cuales solo conoce la interfaz. Estos no cuentan como amigos porque no se impone ninguna expectativa sobre cómo conseguirlos. Si el objeto no sabe de dónde vino porque algo más lo pasó / lo inyectó, entonces no es amigo de un amigo, ni siquiera es un amigo. Es algo que el objeto solo sabe usar. Eso es bueno.

Cuando se trata de aplicar principios como este, es importante comprender que nunca te están prohibiendo lograr algo. Son una advertencia de que puede estar descuidando hacer más trabajo para lograr un mejor diseño que logre lo mismo.

Nadie quiere trabajar sin razón alguna, por lo que es importante entender lo que se obtiene al seguir esto. En este caso, mantiene su código flexible. Puede hacer cambios y tener menos clases afectadas por los cambios de los que preocuparse. Eso suena bien, pero no te ayuda a decidir qué hacer a menos que lo tomes como una especie de doctrina religiosa.

En lugar de seguir ciegamente este principio, tome una versión simple de este problema. Escriba una solución que no siga el principio y una que sí lo haga. Ahora que tiene dos soluciones, puede comparar cuán receptivo es cada una a los cambios tratando de hacerlas en ambas.

Si NO PUEDES resolver un problema mientras sigues este principio, es probable que te falte otra habilidad.

Una solución a su problema particular es inyectarlo frameen algo (una clase o método) que sepa cómo hablar con marcos para que no tenga que difundir todos esos detalles de chat de marcos en su clase, que ahora solo sabe cómo y cuándo hacerlo. conseguir un marco

Eso en realidad sigue otro principio: uso separado de la construcción.

frame = encoder->WaitEncoderFrame()

Al usar este código, te has responsabilizado de alguna manera de adquirir un Frame. Aún no has asumido ninguna responsabilidad por hablar con a Frame.

frame->DoOrGetSomething(); 

Ahora debe saber cómo hablar con a Frame, pero reemplácelo con:

new FrameHandler(frame)->DoOrGetSomething();

Y ahora solo tienes que saber cómo hablar con tu amigo, FrameHandler.

Hay muchas maneras de lograr esto y esta quizás no sea la mejor, pero muestra cómo seguir el principio no resuelve el problema. Solo te exige más trabajo.

Toda buena regla tiene una excepción. Los mejores ejemplos que conozco son los lenguajes internos específicos del dominio . A DSL s cadena métodoparece abusar de la Ley de Deméter todo el tiempo porque constantemente estás llamando a métodos que devuelven diferentes tipos y usándolos directamente. ¿Por qué está bien esto? Porque en un DSL todo lo que se devuelve es un amigo cuidadosamente diseñado con el que debes hablar directamente. Por diseño, se le ha dado el derecho de esperar que la cadena de métodos de una DSL no cambie. No tienes ese derecho si simplemente profundizas en la base del código encadenando lo que encuentres. Los mejores DSL son representaciones o interfaces muy delgadas con otros objetos en los que probablemente tampoco debería profundizar. Solo menciono esto porque descubrí que entendía la ley del demeter mucho mejor una vez que aprendí por qué las DSL son un buen diseño. Algunos van tan lejos como para decir que las DSL ni siquiera violan la ley real de demeter.

Otra solución es dejar que algo más te inyecte frame. Si framevino de un instalador o, preferiblemente, de un constructor, entonces no asume ninguna responsabilidad por construir o adquirir un marco. Esto significa que su papel aquí es mucho más como FrameHandlerslo que iba a ser. En cambio, ahora eres el que habla Framey hace que otra cosa descubra cómo obtener un Frame En cierta forma, esta es la misma solución con un simple cambio de perspectiva.

Los principios SÓLIDOS son los más importantes que trato de seguir. Aquí se respetan dos principios de responsabilidad única e inversión de dependencia. Es realmente difícil respetar a estos dos y aun así terminar violando la Ley de Deméter.

La mentalidad que implica violar a Demeter es como comer en un restaurante buffet donde simplemente agarras lo que quieras. Con un poco de trabajo por adelantado, puede proporcionarse un menú y un servidor que le brindará lo que quiera. Siéntese, relájese y dé una buena propina.

naranja confitada
fuente
2
"Te dice que es mejor hablar solo con tus" amigos "y no con" amigos de amigos "" ++
RubberDuck
1
El segundo párrafo, ¿es eso robado de alguna parte, o lo inventaste? Eso me hizo entender el concepto por completo . Hizo evidente lo que son los "amigos de amigos" y los inconvenientes que conlleva enredar demasiadas clases (articulaciones). A + cita.
die maus
2
@diemaus Si robé ese párrafo de escudo de algún lugar, su fuente se ha filtrado desde mi cabeza. En ese momento mi cerebro solo pensaba que estaba siendo inteligente. Lo que sí recuerdo es que lo agregué después de muchos de los votos positivos, así que es bueno verlo validado. Me alegra que haya ayudado.
candied_orange
19

¿Es el diseño funcional mejor que el diseño orientado a objetos? Depende.

¿MVVM es mejor que MVC? Depende.

Amos y Andy o Martin y Lewis? Depende.

¿De qué depende? Las elecciones que haga dependerán de qué tan bien cada técnica o tecnología cumpla con los requisitos funcionales y no funcionales de su software, al tiempo que satisface adecuadamente sus objetivos de diseño, rendimiento y mantenibilidad.

[Algún libro] dice que [algo] está mal.

Cuando lea esto en un libro o blog, evalúe el reclamo según su mérito; es decir, pregunta por qué. No existe una técnica correcta o incorrecta en el desarrollo de software, solo existe "¿cuán bien cumple esta técnica con mis objetivos? ¿Es efectiva o ineficaz? ¿Resuelve un problema pero crea uno nuevo? ¿Puede ser entendido bien por todo el equipo? equipo de desarrollo, o es demasiado oscuro? "

En este caso particular, el acto de invocar un método en un objeto devuelto por otro método, dado que existe un patrón de diseño real que codifica esta práctica (Factory), es difícil imaginar cómo se podría afirmar que es categórica incorrecto.

La razón por la que se llama el "Principio de menor conocimiento" es que el "Acoplamiento bajo" es una cualidad deseable de un sistema. Los objetos que no están estrechamente unidos entre sí funcionan de manera más independiente y, por lo tanto, son más fáciles de mantener y modificar individualmente. Pero como muestra su ejemplo, hay momentos en que el acoplamiento alto es más deseable, por lo que los objetos pueden coordinar sus esfuerzos de manera más efectiva.

Robert Harvey
fuente
2

La respuesta de Doc Brown muestra una implementación clásica de la Ley de Deméter en los libros de texto, y la molestia / hinchazón de código desorganizado de agregar docenas de métodos de esa manera es probablemente la razón por la cual los programadores, incluido yo mismo, a menudo no nos molestamos en hacerlo, incluso si deberían hacerlo.

Hay una forma alternativa de desacoplar la jerarquía de objetos:

Exponga interfacetipos, en lugar de classtipos, a través de sus métodos y propiedades.

En el caso del Cartel original (OP), encoder->WaitEncoderFrame()devolvería un en IEncoderFramelugar de un Frame, y definiría qué operaciones son permisibles.


SOLUCION 1

En el caso más fácil, Framey las Encoderclases están bajo su control, IEncoderFramees un subconjunto de métodos que Frame ya expone públicamente, y a la Encoderclase en realidad no le importa lo que haga con ese objeto. Entonces, la implementación es trivial ( código en c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

SOLUCIÓN 2

En un caso intermedio, donde la Framedefinición no está bajo su control, o no sería apropiado agregar IEncoderFramelos métodos Frame, entonces una buena solución es un Adaptador . Eso es lo que discute la respuesta de CandiedOrange , como new FrameHandler( frame ). IMPORTANTE: si hace esto, es más flexible si lo expone como una interfaz , no como una clase . Encodertiene que saber class FrameHandler, pero los clientes solo necesitan saberlo interface IFrameHandler. O como lo llamé, interface IEncoderFramepara indicar que es específicamente Frame como se ve desde POV de Encoder :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

COSTO: Asignación y GC de un nuevo objeto, EncoderFrameWrapper, cada vez que encoder.TheFramese llama. (Puede almacenar en caché ese contenedor, pero eso agrega más código. Y solo es fácil de codificar de manera confiable si el campo de marco del codificador no se puede reemplazar con un nuevo marco).


SOLUCIÓN 3

En el caso más difícil, el nuevo contenedor necesitaría saber sobre ambos Encodery Frame. Ese objeto en sí mismo violaría LoD: está manipulando una relación entre Encoder y Frame que debería ser responsabilidad de Encoder, y probablemente sea un dolor hacerlo bien. Esto es lo que puede suceder si comienzas por ese camino:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

Eso se puso feo. Hay una implementación menos complicada, cuando el contenedor necesita tocar los detalles de su creador / propietario (Encoder):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

De acuerdo, si supiera que terminaría aquí, no podría hacer esto. Podría simplemente escribir los métodos LoD y terminar con eso. No es necesario definir una interfaz. Por otro lado, me gusta que la interfaz envuelva métodos relacionados juntos. Me gusta cómo se siente hacer las "operaciones tipo marco" a lo que se siente como un marco.


COMENTARIOS FINALES

Considere esto: si el implementador Encoderconsidera que la exposición Frame frameera apropiada para su arquitectura general, o era "mucho más fácil que implementar LoD", entonces habría sido mucho más seguro si en su lugar hicieran el primer fragmento que muestro: exponga un subconjunto limitado de Marco, como una interfaz. En mi experiencia, esa es a menudo una solución completamente viable. Simplemente agregue métodos a la interfaz según sea necesario. (Estoy hablando de un escenario en el que "sabemos" que Frame ya tiene los métodos necesarios, o sería fácil y no controversial agregarlos. El trabajo de "implementación" para cada método es agregar una línea a la definición de la interfaz). Sabemos que incluso en el peor escenario futuro, es posible mantener esa API funcionando: aquí,IEncoderFrameFrameEncoder.

También tenga en cuenta que si usted no tiene permiso para añadir IEncoderFramea Frame, o los métodos necesarios no se ajustan bien a lo general Framede clase, y la solución # 2 no le conviene, quizás debido a objeto adicional creación y destrucción, La solución n. ° 3 puede verse simplemente como una forma de organizar los métodos Encoderpara lograr LoD. No solo pases por docenas de métodos. Envuélvalos en una interfaz y use "implementación de interfaz explícita" (si está en c #), de modo que solo se pueda acceder a ellos cuando el objeto se vea a través de esa interfaz.

Otro punto que quiero enfatizar es que la decisión de exponer la funcionalidad como una interfaz , manejó las 3 situaciones descritas anteriormente. En el primero, IEncoderFramees simplemente un subconjunto de Framela funcionalidad de. En el segundo, IEncoderFramees un adaptador. En el tercero, IEncoderFramehay una partición en Encoderla funcionalidad de s. No importa si sus necesidades cambian entre estas tres situaciones: la API permanece igual.

ToolmakerSteve
fuente
Acoplando su clase a una interfaz devuelta por uno de sus colaboradores, en lugar de una clase concreta, mientras que una mejora, sigue siendo una fuente de acoplamiento. Si la interfaz necesita cambiar, significa que su clase tendrá que cambiar. El punto importante a eliminar es que el acoplamiento no es inherentemente malo siempre y cuando se asegure de evitar una pareja innecesaria con objetos inestables o estructuras internas . Es por eso que la Ley de Deméter debe tomarse con una gran pizca de sal; le indica que evite siempre algo que puede o no ser un problema, según las circunstancias.
Periata Breatta
@PeriataBreatta: ciertamente no puedo y no estoy en desacuerdo con eso. Pero me gustaría señalar una cosa: una interfaz , por definición, representa lo que se necesita saber en el límite entre las dos clases. Si "necesita cambiar", eso es fundamental : ningún enfoque alternativo podría haber evitado mágicamente de alguna manera la codificación necesaria. Compare eso con cualquiera de las tres situaciones que describo, pero sin usar una interfaz; en su lugar, devuelva una clase concreta. En 1, Frame, en 2, EncoderFrameWrapper, en 3, Encoder. Te encierras en ese enfoque. La interfaz puede adaptarse a todos ellos.
ToolmakerSteve
@PeriataBreatta ... que demuestra el beneficio de definirlo explícitamente como una interfaz. Espero eventualmente mejorar un IDE para que sea tan conveniente, que las interfaces se usen mucho más. La mayoría de los accesos multinivel serían a través de alguna interfaz, por lo tanto, sería mucho más fácil administrar los cambios. (Si eso es "excesivo" para algunos casos, el análisis de código, combinado con anotaciones sobre dónde estamos dispuestos a correr el riesgo de no tener una interfaz, a cambio del pequeño aumento del rendimiento, podría "compilarlo", reemplazándolo por una de esas 3 clases concretas de las 3 "soluciones".)
ToolmakerSteve
@PeriataBreatta - y cuando digo "la interfaz puede adaptarse a todos ellos", no digo "ahhhh, es maravilloso que esta característica de un idioma pueda cubrir estos diferentes casos". Estoy diciendo que definir interfaces minimiza los cambios que podría necesitar hacer. En el mejor de los casos, el diseño puede cambiar desde el caso más simple (solución 1) hasta el caso más difícil (solución 3), sin que la interfaz cambie en absoluto; solo las necesidades internas del productor se han vuelto más complejas. E incluso cuando se necesitan cambios, tienden a ser menos penetrantes, en mi humilde opinión.
ToolmakerSteve