Geek Time-The Beauty of Design Patterns Theory 11: How to use the Law of Demeter (LOD) to achieve "high cohesion and loose coupling"?

Preface

● What is "high cohesion and loose coupling"?

● How to use Dimit's law to achieve "high cohesion and loose coupling"?

● What code designs are clearly against Dimit's law? How to reconstruct this?

What is "high cohesion and loose coupling"?

"High cohesion and loose coupling" is a very important design idea, which can effectively improve the readability and maintainability of the code, and reduce the scope of code changes caused by functional changes. In fact, in the previous chapters, we have mentioned this design idea many times. Many design principles are aimed at achieving "high cohesion and loose coupling" of code, such as the single responsibility principle, based on interfaces rather than implementation programming.

In fact, "high cohesion and loose coupling" is a more general design idea, which can be used to guide the design and development of different granular codes, such as systems, modules, classes, and even functions, and can also be applied to different development scenarios , Such as microservices, frameworks, components, class libraries, etc. In order to facilitate my explanation, I will use "class" as the application object of this design idea to expand the explanation. You can compare other application scenarios by yourself.

In this design philosophy, "high cohesion" is used to guide the design of the class itself, and "loose coupling" is used to guide the design of the dependencies between classes. However, the two are not completely independent and unrelated. High cohesion helps loose coupling, and loose coupling requires the support of high cohesion.

So what exactly is "high cohesion"?

The so-called high cohesion means that similar functions should be placed in the same class, and dissimilar functions should not be placed in the same class. Similar functions are often modified at the same time, put in the same class, the modification will be more concentrated, and the code is easy to maintain. In fact, the single responsibility principle we mentioned earlier is a very effective design principle for achieving high code cohesion.

Let's take a look again, what is "loose coupling"?

The so-called loose coupling means that in the code, the dependencies between classes are simple and clear. Even if two classes have a dependency relationship, code changes in one class will not or rarely lead to code changes in the dependent class. In fact, the dependency injection, interface isolation, programming based on interfaces rather than implementation, and Dimit's rule we talked about today are all for the loose coupling of code.

Theoretical description of "Dimit's Law"

The English translation of the Law of Demeter is: Law of Demeter, abbreviated as LOD. Judging from the name alone, we can't guess what this principle is about. However, it has another more expressive name, called the Principle of Least Knowledge, which is translated in English as: The Least Knowledge Principle.

Regarding this design principle, let’s take a look at its most original English definition:

Each unit should have only limited knowledge about other units: only
units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

We literally translate it into Chinese, it looks like this:

Each module (unit) should only understand the limited knowledge of those modules (units: only units “closely” related to the current unit). In other words, each module only "talks" with its own friends, and does not "talk" with strangers.

As we said before, most of the design principles and ideas are very abstract, and there are various interpretations. To apply them flexibly to actual development, it requires the accumulation of actual combat experience. Dimit's Law is no exception. Therefore, I combined my own understanding and experience to re-describe the definition just now. Note that, in order to explain uniformly, I replaced the "module" in the definition description with "class".

Between classes that should not have direct dependencies, do not have dependencies; between classes that have dependencies, try to only rely on necessary interfaces (that is, the "limited knowledge" in the definition).

From the above description, we can see that Dimit's Law consists of two parts before and after. These two parts talk about two things. I will use two actual cases to interpret them separately.

Theoretical interpretation and code actual combat one

Let's first look at the first half of this principle, "There should be no dependencies between classes that should not have direct dependencies . " Let me explain with an example.

This example implements a simplified version of the search engine crawling web pages. The code contains three main classes. Among them, the NetworkTransporter class is responsible for the underlying network communication and obtain data according to the request; the HtmlDownloader class is used to obtain web pages through URLs ; Document represents a web document, and subsequent web content extraction, word segmentation, and indexing are all processed based on this. The specific code implementation is as follows:


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);
  }
  //...
}

Although this code is "usable" and can achieve the functions we want, it is not "usable" enough and has many design flaws. You can try to think about it first and see what are the flaws, and then look at my explanation below

First, let's look at the NetworkTransporter class. As a low-level network communication class, we hope that its function is as general as possible, not just for downloading HTML, so we should not directly rely on the too specific sending object HtmlRequest. From this point of view, the design of the NetworkTransporter class violates Dimit's law and relies on the HtmlRequest class that should not have a direct dependency.

How should we refactor to make the NetworkTransporter class satisfy Dimit's law? I have a vivid analogy here. If you are going to a store to buy something, you will definitely not directly give the wallet to the cashier and let the cashier take the money from it, but you take the money out of the wallet and give it to the cashier. The HtmlRequest object here is equivalent to a wallet, and the address and content objects in the HtmlRequest are equivalent to money. We should pass the address and content to NetworkTransporter instead of directly passing HtmlRequest to NetworkTransporter . According to this idea, the refactored code of NetworkTransporter is as follows:


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

Let's look at the HtmlDownloader class again. There is no problem with the design of this class. However, we have modified the definition of the send() function of NetworkTransporter, and this class uses the send() function, so we need to modify it accordingly. The modified code is as follows:


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);
  }
}

Finally, we look at the Document class . There are many problems in this category, mainly in three points. First, the downloader.downloadHtml() in the constructor is complicated in logic and time-consuming, and should not be placed in the constructor, which will affect the testability of the code. We will talk about the testability of the code later, here you only need to know this. Second, the HtmlDownloader object is created by new in the constructor, which violates the design philosophy based on interfaces instead of programming, and also affects the testability of the code. Third, in terms of business implications, Document web documents do not need to rely on the HtmlDownloader class, which violates Dimit's Law.

Although there are many problems with the Document class, it is relatively simple to modify, and all problems can be solved with one modification. The modified code is as follows:


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);
  }
}

Theoretical interpretation and code actual combat II

Now, let's take a look at the second half of this principle: "between classes with dependencies, try to only rely on necessary interfaces". Let's explain with an example. The following code is very simple, the Serialization class is responsible for the serialization and deserialization of objects.


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

Just look at the design of this class, there is no problem. However, if we put it in a certain application scenario, there is still room for continued optimization. Suppose that in our project, some classes only use serialization operations, while other classes only use deserialization operations. Based on the second half of Dimit's rule, "between dependent classes, try to only rely on the necessary interface", the part of the class that only uses the serialization operation should not rely on the deserialization interface. Similarly, the part of the class that only uses the deserialization operation should not rely on the serialization interface.

According to this idea, we should split the Serialization class into two smaller-grained classes, one is only responsible for serialization (Serializer class), and the other is only responsible for deserialization (Deserializer class). After the split, the class that uses the serialization operation only needs to rely on the Serializer class, and the class that uses the deserialization operation only needs to rely on the Deserializer class. The code after the split is as follows:


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

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

I don't know if you can see that although the code after splitting can better satisfy Dimit's law, it violates the design philosophy of high cohesion. High cohesion requires similar functions to be placed in the same class, so that when the functions are modified, the modified places will not be too scattered. For the example just now, if we modify the implementation of serialization, such as from JSON to XML, the implementation logic of deserialization also needs to be modified. Without splitting, we only need to modify one class. After splitting, we need to modify two classes. Obviously, the scope of code changes for this design idea has become larger.

If we neither want to violate the design philosophy of high cohesion, nor the law of Dimit, how can we solve this problem? In fact, this problem can be easily solved by introducing two interfaces. The specific code is shown below.


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;
  }
  //...
}

Although we still have to pass in the Serialization implementation class that contains serialization and deserialization into the constructor of DemoClass_1, the Serializable interface we rely on only contains serialization operations, and DemoClass_1 cannot use the deserialization interface in the Serialization class. No perception of deserialization operations, which also meets the requirement of "relying on limited interfaces" as mentioned in the second half of Dimit's rule.

In fact, the above code implementation ideas also reflect the design principle of "based on interface rather than implementation programming". Combining Dimit's rule, we can conclude a new design principle, which is "based on minimum interface instead of maximum Realize programming". Some students asked before how the new design patterns and design principles were created. In fact, it was a routine that was summarized for development pain points in a lot of practice.

Dialectical thinking and flexible application

Do you have any different views on the final design idea of ​​Actual Combat II?

The entire class only contains two operations, serialization and deserialization, and users who only use serialization operations, even if they can perceive only one deserialization function, the problem is not big. So in order to satisfy Dimit's law, we split a very simple class into two interfaces. Is it a bit over-designed?

The design principle itself is not right or wrong, only whether it can be used. Do not apply design principles for the sake of applying design principles. When we apply design principles, we must analyze specific issues in detail.

For the Serialization class just now, it only contains two operations, and there is really no need to split it into two interfaces. However, if we add more functions to the Serialization class and implement more and more useful serialization and deserialization functions, let's reconsider this issue. The modified specific code is as follows:


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) {
    
     //... }
}

In this scenario, the second design idea is better. Because based on the previous application scenario, most of the code only needs to use the serialization function. For these users, there is no need to understand the "knowledge" of deserialization, and the modified Serialization class, the "knowledge" of deserialization, has changed from one function to three. Once any deserialization operation has code changes, we need to check and test whether all the code that depends on the Serialization class can still work normally. In order to reduce the coupling and testing workload, we should separate the deserialization and serialization functions according to Dimit's law.

Key review

1. How to understand "high cohesion and loose coupling"? "

"High cohesion and loose coupling" is a very important design idea, which can effectively improve the readability and maintainability of the code, and reduce the scope of code changes caused by functional changes. "High cohesion" is used to guide the design of the class itself, and "loose coupling" is used to guide the design of the dependencies between classes. The so-called high cohesion means that similar functions should be placed in the same category, and dissimilar functions should not be placed in the same category. Similar functions are often modified at the same time, put in the same category, the modification will be more concentrated. The so-called loose coupling means that in the code, the dependencies between classes are simple and clear. Even if two classes have a dependency relationship, code changes in one class will not or rarely lead to code changes in the dependent class.

2. How to understand the "Law of Demeter"?
Between classes that should not have direct dependencies, don't have dependencies; between classes that have dependencies, try to only rely on necessary interfaces. Dimit's law is to reduce the coupling between classes, so that the more independent the better. Each class should know less about the other parts of the system. Once a change occurs, there are fewer classes that need to understand the change.

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110440307