Geek Time-The Beauty of Design Patterns Theory 11: ¿Cómo utilizar la Ley de Demeter (LOD) para lograr "alta cohesión y acoplamiento flexible"?

Prefacio

● ¿Qué es "alta cohesión y acoplamiento flexible"?

● ¿Cómo utilizar la ley de Dimit para lograr "alta cohesión y acoplamiento flexible"?

● ¿Qué diseños de código están claramente en contra de la ley de Dimit? ¿Cómo reconstruir esto?

¿Qué es "alta cohesión y acoplamiento flexible"?

"Alta cohesión y acoplamiento flexible" es una idea de diseño muy importante, que puede mejorar eficazmente la legibilidad y el mantenimiento del código, y reducir el alcance de los cambios de código causados ​​por cambios funcionales. De hecho, en los capítulos anteriores, hemos mencionado esta idea de diseño muchas veces. Muchos principios de diseño tienen como objetivo lograr "alta cohesión y acoplamiento flexible" de código, como el principio de responsabilidad única, basado en interfaces en lugar de programación de implementación.

De hecho, "alta cohesión y acoplamiento flexible" es una idea de diseño más general, que se puede utilizar para guiar el diseño y desarrollo de diferentes códigos granulares, como sistemas, módulos, clases e incluso funciones, y también se puede aplicar a diferentes escenarios de desarrollo. , Como microservicios, marcos, componentes, bibliotecas de clases, etc. Para facilitar mi explicación, utilizaré "clase" como el objeto de aplicación de esta idea de diseño para ampliar la explicación. Puede comparar otros escenarios de aplicación usted mismo.

En esta filosofía de diseño, se utiliza "alta cohesión" para guiar el diseño de la clase en sí, y "acoplamiento suelto" se utiliza para guiar el diseño de las dependencias entre clases. Sin embargo, los dos no son completamente independientes y no están relacionados. La alta cohesión ayuda a un acoplamiento flojo y un acoplamiento flojo requiere el apoyo de una alta cohesión.

Entonces, ¿qué es exactamente "alta cohesión"?

La llamada alta cohesión significa que las funciones similares deben colocarse en la misma clase y las funciones diferentes no deben colocarse en la misma clase. Las funciones similares a menudo se modifican al mismo tiempo, se colocan en la misma clase, la modificación será más concentrada y el código es fácil de mantener. De hecho, el principio de responsabilidad única que mencionamos anteriormente es un principio de diseño muy eficaz para lograr una alta cohesión del código.

Echemos un vistazo de nuevo, ¿qué es "acoplamiento flojo"?

El llamado acoplamiento suelto significa que en el código, las dependencias entre clases son simples y claras. Incluso si dos clases tienen una relación de dependencia, los cambios de código en una clase no conducirán o rara vez conducirán a cambios de código en la clase dependiente. De hecho, la inyección de dependencias, el aislamiento de interfaces, la programación basada en interfaces en lugar de la implementación y la regla de Dimit de la que hablamos hoy son todas para el acoplamiento flexible de código.

Descripción teórica de la "Ley de Dimit"

La traducción al inglés de la Ley de Deméter es: Ley de Deméter, abreviada como LOD. A juzgar solo por el nombre, no podemos adivinar de qué se trata este principio. Sin embargo, tiene otro nombre más expresivo, llamado Principio de conocimiento mínimo, que se traduce en inglés como: Principio de conocimiento mínimo.

Con respecto a este principio de diseño, echemos un vistazo a su definición en inglés más original:

Cada unidad debe tener un conocimiento limitado sobre otras unidades: solo
unidades "estrechamente" relacionadas con la unidad actual. O: cada unidad solo debe hablar con sus amigos; No hables con extraños.

Lo traducimos literalmente al chino, se ve así:

Cada módulo (unidad) solo debe comprender el conocimiento limitado de esos módulos (unidades: solo unidades "estrechamente" relacionadas con la unidad actual). En otras palabras, cada módulo sólo "habla" con sus propios amigos y no "habla" con extraños.

Como dijimos antes, la mayoría de los principios e ideas de diseño son muy abstractos y hay varias interpretaciones, para aplicarlos de manera flexible al desarrollo real, se requiere la acumulación de experiencia de combate real. La ley de Dimit no es una excepción. Entonces, combiné mi propia comprensión y experiencia para volver a describir la definición en este momento. Tenga en cuenta que, para explicar de manera uniforme, reemplacé el "módulo" en la descripción de la definición con "clase".

Entre clases que no deben tener dependencias directas, no tienen dependencias, entre clases que tienen dependencias, trate de confiar solo en las interfaces necesarias (es decir, el "conocimiento limitado" en la definición).

De la descripción anterior, podemos ver que la Ley de Dimit consta de dos partes antes y después. Estas dos partes hablan de dos cosas. Usaré dos casos reales para interpretarlas por separado.

Interpretación teórica y código de combate real uno.

Veamos primero la primera mitad de este principio, "No debería haber dependencias entre clases que no deberían tener dependencias directas" . Dejame explicarte con un ejemplo.

Este ejemplo implementa una versión simplificada del motor de búsqueda que rastrea las páginas web. El código contiene tres clases principales. Entre ellos, la clase NetworkTransporter es responsable de la comunicación de red subyacente y obtiene datos de acuerdo con la solicitud; la clase HtmlDownloader se usa para obtener páginas web a través de URL ; Document representa un documento web, y la posterior extracción de contenido web, segmentación de palabras e indexación se procesan en base a esto. La implementación del código específico es la siguiente:


public class NetworkTransporter {
    
    
    // 省略属性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
    
    
      //...
    }
}

public class HtmlDownloader {
    
    
  private NetworkTransporter transporter;//通过构造函数或IOC注入
  
  public Html downloadHtml(String url) {
    
    
    Byte[] rawHtml = transporter.send(new HtmlRequest(url));
    return new Html(rawHtml);
  }
}

public class Document {
    
    
  private Html html;
  private String url;
  
  public Document(String url) {
    
    
    this.url = url;
    HtmlDownloader downloader = new HtmlDownloader();
    this.html = downloader.downloadHtml(url);
  }
  //...
}

Aunque este código es "utilizable" y puede lograr las funciones que queremos, no es lo suficientemente "utilizable" y tiene muchos defectos de diseño. Puede intentar pensar en ello primero y ver cuáles son los defectos, y luego mirar mi explicación a continuación.

Primero, veamos la clase NetworkTransporter . Como clase de comunicación de red de bajo nivel, esperamos que su función sea lo más general posible, no solo para descargar HTML, por lo que no debemos confiar directamente en el objeto de envío demasiado específico HtmlRequest. Desde este punto de vista, el diseño de la clase NetworkTransporter viola la ley de Dimit y se basa en la clase HtmlRequest que no debería tener una dependencia directa.

¿Cómo deberíamos refactorizar para que la clase NetworkTransporter satisfaga la ley de Dimit? Aquí tengo una vívida analogía. Si va a ir a la tienda a comprar cosas ahora, definitivamente no le dará la billetera directamente al cajero y dejará que el cajero tome el dinero, sino que saque el dinero de la billetera y se lo dé al cajero. El objeto HtmlRequest aquí es equivalente a una billetera, y los objetos de dirección y contenido en HtmlRequest son equivalentes a dinero. Deberíamos pasar la dirección y el contenido a NetworkTransporter en lugar de pasar directamente HtmlRequest a NetworkTransporter . Según esta idea, el código refactorizado de NetworkTransporter es el siguiente:


public class NetworkTransporter {
    
    
    // 省略属性和其他方法...
    public Byte[] send(String address, Byte[] data) {
    
    
      //...
    }
}

Veamos nuevamente la clase HtmlDownloader . No hay ningún problema con el diseño de esta clase. Sin embargo, hemos modificado la definición de la función send () de NetworkTransporter, y esta clase usa la función send (), por lo que debemos modificarla en consecuencia. El código modificado es el siguiente:


public class HtmlDownloader {
    
    
  private NetworkTransporter transporter;//通过构造函数或IOC注入
  
  // HtmlDownloader这里也要有相应的修改
  public Html downloadHtml(String url) {
    
    
    HtmlRequest htmlRequest = new HtmlRequest(url);
    Byte[] rawHtml = transporter.send(
      htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
    return new Html(rawHtml);
  }
}

Finalmente, miramos la clase Documento . Hay muchos problemas en esta categoría, principalmente en tres puntos. Primero, el downloader.downloadHtml () en el constructor es complicado en lógica y requiere mucho tiempo, y no debe colocarse en el constructor, lo que afectará la capacidad de prueba del código. Hablaremos de la testabilidad del código más adelante, aquí solo necesitas saber esto. En segundo lugar, el objeto HtmlDownloader es creado por new en el constructor, lo que viola la filosofía de diseño basada en interfaces en lugar de programación, y también afecta la capacidad de prueba del código. En tercer lugar, en términos de implicaciones comerciales, los documentos web de Document no necesitan depender de la clase HtmlDownloader, que viola la ley de Dimit.

Aunque hay muchos problemas con la clase Document, es relativamente simple de modificar y todos los problemas se pueden resolver con una sola modificación. El código modificado es el siguiente:


public class Document {
    
    
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    
    
    this.html = html;
    this.url = url;
  }
  //...
}

// 通过一个工厂方法来创建Document
public class DocumentFactory {
    
    
  private HtmlDownloader downloader;
  
  public DocumentFactory(HtmlDownloader downloader) {
    
    
    this.downloader = downloader;
  }
  
  public Document createDocument(String url) {
    
    
    Html html = downloader.downloadHtml(url);
    return new Document(url, html);
  }
}

Interpretación teórica y código de combate real II

Ahora, echemos un vistazo a la segunda mitad de este principio: "entre clases con dependencias, intente confiar solo en las interfaces necesarias". Expliquemos con un ejemplo. El siguiente código es muy simple, la clase Serialization es responsable de la serialización y deserialización de objetos.


public class Serialization {
    
    
  public String serialize(Object object) {
    
    
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    
    
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}

Basta con mirar el diseño de esta clase, no hay problema. Sin embargo, si lo ponemos en un escenario de aplicación determinado, todavía hay espacio para una optimización continua. Supongamos que en nuestro proyecto, algunas clases solo usan operaciones de serialización, mientras que otras clases solo usan operaciones de deserialización. Basado en la segunda mitad de la regla de Dimit, "entre clases dependientes, intente confiar solo en las interfaces necesarias", la parte de las clases que solo usan la operación de serialización no debe depender de la interfaz de deserialización. De manera similar, la parte de la clase que solo usa la operación de deserialización no debe depender de la interfaz de serialización.

De acuerdo con esta idea, deberíamos dividir la clase Serialización en dos clases de grano más pequeño, una solo es responsable de la serialización (clase Serializer) y la otra solo es responsable de la deserialización (clase Deserializer). Después de la división, la clase que usa la operación de serialización solo necesita depender de la clase Serializer y la clase que usa la operación de deserialización solo necesita depender de la clase Deserializer. El código después de la división es el siguiente:


public class Serializer {
    
    
  public String serialize(Object object) {
    
    
    String serializedResult = ...;
    ...
    return serializedResult;
  }
}

public class Deserializer {
    
    
  public Object deserialize(String str) {
    
    
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

No sé si puede ver que aunque el código después de la división puede satisfacer mejor la ley de Dimit, viola la filosofía de diseño de alta cohesión. Una alta cohesión requiere que funciones similares se coloquen en la misma clase, de modo que cuando se modifiquen las funciones, los lugares modificados no estén demasiado dispersos. Para el ejemplo de ahora, si modificamos la implementación de la serialización, como de JSON a XML, la lógica de implementación de la deserialización también debe modificarse. Sin dividir, solo necesitamos modificar una clase. Después de dividir, necesitamos modificar dos clases. Obviamente, el alcance de los cambios de código para esta idea de diseño se ha vuelto más grande.

Si no queremos violar la filosofía de diseño de alta cohesión, ni la Ley de Dimit, ¿cómo podemos solucionar este problema? De hecho, este problema se puede solucionar fácilmente introduciendo dos interfaces, el código específico se muestra a continuación.


public interface Serializable {
    
    
  String serialize(Object object);
}

public interface Deserializable {
    
    
  Object deserialize(String text);
}

public class Serialization implements Serializable, Deserializable {
    
    
  @Override
  public String serialize(Object object) {
    
    
    String serializedResult = ...;
    ...
    return serializedResult;
  }
  
  @Override
  public Object deserialize(String str) {
    
    
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

public class DemoClass_1 {
    
    
  private Serializable serializer;
  
  public Demo(Serializable serializer) {
    
    
    this.serializer = serializer;
  }
  //...
}

public class DemoClass_2 {
    
    
  private Deserializable deserializer;
  
  public Demo(Deserializable deserializer) {
    
    
    this.deserializer = deserializer;
  }
  //...
}

Aunque todavía tenemos que pasar la clase de implementación de serialización que contiene serialización y deserialización al constructor de DemoClass_1, la interfaz serializable en la que confiamos solo contiene operaciones de serialización, y DemoClass_1 no puede usar la interfaz de deserialización en la clase de serialización. Sin percepción de operaciones de deserialización, que también cumple con el requisito de "confiar en interfaces limitadas" como se menciona en la segunda mitad de la regla de Dimit.

De hecho, las ideas de implementación de código anteriores también reflejan el principio de diseño de "basado en la interfaz en lugar de la programación de implementación". Combinando la regla de Dimit, podemos concluir un nuevo principio de diseño, que está "basado en una interfaz mínima en lugar de una Realizar programación ". Algunos estudiantes preguntaron antes cómo se crearon los nuevos patrones de diseño y principios de diseño. De hecho, era una rutina que se resumía para los puntos débiles del desarrollo en mucha práctica.

Pensamiento dialéctico y aplicación flexible

¿Tienes diferentes puntos de vista sobre la idea de diseño final de Actual Combat II?

Toda la clase solo contiene dos operaciones, serialización y deserialización, y usuarios que solo usan operaciones de serialización, incluso si pueden percibir solo una función de deserialización, el problema no es grande. Entonces, para satisfacer la ley de Dimit, dividimos una clase muy simple en dos interfaces. ¿Está un poco sobre-diseñado?

El principio de diseño en sí no es correcto o incorrecto, solo si se puede usar. No aplique principios de diseño por el simple hecho de aplicar principios de diseño. Cuando aplicamos principios de diseño, debemos analizar cuestiones específicas en detalle.

Para la clase de serialización de ahora, solo contiene dos operaciones, y realmente no hay necesidad de dividirla en dos interfaces. Sin embargo, si agregamos más funciones a la clase de serialización e implementamos más y más funciones de serialización y deserialización útiles, reconsideremos este problema. El código específico modificado es el siguiente:


public class Serializer {
    
     // 参看JSON的接口定义
  public String serialize(Object object) {
    
     //... }
  public String serializeMap(Map map) {
    
     //... }
  public String serializeList(List list) {
    
     //... }
  
  public Object deserialize(String objectString) {
    
     //... }
  public Map deserializeMap(String mapString) {
    
     //... }
  public List deserializeList(String listString) {
    
     //... }
}

En este escenario, la segunda idea de diseño es mejor. Porque según el escenario de aplicación anterior, la mayor parte del código solo necesita usar la función de serialización. Para estos usuarios, no es necesario comprender el "conocimiento" de la deserialización, y la clase de serialización modificada, el "conocimiento" de la deserialización, ha cambiado de una función a tres. Una vez que cualquier operación de deserialización tiene cambios de código, debemos verificar y probar si todo el código que depende de la clase de serialización aún puede funcionar normalmente. Para reducir la carga de trabajo de acoplamiento y prueba, debemos separar las funciones de deserialización y serialización de acuerdo con la ley de Dimit.

Revisión clave

1. ¿Cómo entender "alta cohesión y acoplamiento flexible"? "

"Alta cohesión y acoplamiento flexible" es una idea de diseño muy importante, que puede mejorar de manera efectiva la legibilidad y mantenibilidad del código y reducir el alcance de los cambios de código causados ​​por cambios funcionales. "Alta cohesión" se utiliza para guiar el diseño de la clase en sí, y "acoplamiento flexible" se utiliza para guiar el diseño de las dependencias entre clases. La llamada alta cohesión significa que las funciones similares deben ubicarse en la misma categoría y las funciones diferentes no deben ubicarse en la misma categoría. Las funciones similares a menudo se modifican al mismo tiempo, colocadas en la misma categoría, la modificación será más concentrada. El llamado acoplamiento suelto significa que en el código, las dependencias entre clases son simples y claras. Incluso si dos clases tienen una relación de dependencia, los cambios de código en una clase no conducirán o rara vez conducirán a cambios de código en la clase dependiente.

2. ¿Cómo entender la "Ley de Demeter"?
Entre clases que no deberían tener dependencias directas, no tienen dependencias; entre clases que tienen dependencias, intente confiar solo en las interfaces necesarias. La ley de Dimit es reducir el acoplamiento entre clases, de modo que cuanto más independientes, mejor. Cada clase debería saber menos sobre las otras partes del sistema. Una vez que ocurre un cambio, hay menos clases que necesitan entender el cambio.

Supongo que te gusta

Origin blog.csdn.net/zhujiangtaotaise/article/details/110440307
Recomendado
Clasificación