Geek Time-Design Pattern Theory 6: Why is it based on interfaces instead of programming? Is it necessary to define an interface for each class?

In the previous lesson, we talked about interfaces and abstract classes, and how various programming languages ​​support and implement these two grammatical concepts. Today, we continue to talk about a knowledge point related to "interfaces": programming based on interfaces rather than implementation. This principle is very important. It is a very effective way to improve the quality of the code. It is often used in daily development.

In order for you to understand thoroughly and truly master how this principle is applied, today, I will explain it in conjunction with a practical case of image storage. In addition, this principle can easily be over-applied, such as defining corresponding interfaces for each implementation class. For this kind of problem, in today's explanation, I will also tell you how to make trade-offs and how to apply this principle appropriately.

Not much to say, let's officially start today's learning!

How to interpret the word "interface" in the principle?

The English description of the principle of "based on interface rather than implementation" is: "Program to an interface, not an implementation". When we understand this principle, we must not be tied to a specific programming language at the beginning, and confined to the "interface" grammar of the programming language (such as the interface grammar in Java). This principle first appeared in GoF's "Design Patterns" book in 1994. It was born before many programming languages ​​(such as the Java language). It is a relatively abstract and general design idea.

In fact, the key to understanding this principle is to understand the word "interface". Remember the definition of "interface" we talked about in the last lesson? In essence, an "interface" is a set of "protocols" or "agreements", which is a "function list" provided by a function provider to users. "Interface" will be interpreted differently in different application scenarios, such as the "interface" between the server and the client, the "interface" provided by the class library, and even a set of communication protocols can be called "interfaces" . The understanding of "interface" just now is relatively high-level and abstract, which is a bit far from actual code writing. If it is implemented in specific coding, the "interface" in the principle of "based on interface rather than implementation programming" can be understood as an interface or abstract class in a programming language.

As we mentioned earlier, this principle can be very effective in improving code quality. The reason for saying this is that the application of this principle can separate interfaces and implementations, encapsulate unstable implementations, and expose stable interfaces. The upstream system is interface-oriented rather than implementation programming, and does not rely on unstable implementation details, so that when the implementation changes, the code of the upstream system basically does not need to be changed, so as to reduce coupling and improve scalability.

In fact, another way of expressing the principle of "based on interface rather than implementation programming" is "based on abstraction rather than implementation programming". The latter way of expression actually better reflects the original intention of this principle. In software development, one of the biggest challenges is the constant change of requirements, which is also a standard for testing the quality of code design. **The more abstract, the more top-level, and the more out of the specific implementation of the design, the more flexible the code can be, and the more it can cope with future changes in requirements. Good code design can not only respond to current needs, but also can flexibly respond to changes in future requirements without destroying the original code design. **And abstraction is one of the most effective means to improve code scalability, flexibility, and maintainability.

How to apply this principle to actual combat?

For this principle, we will further explain it in conjunction with a specific actual case.

Assume that our system has a lot of business logic involving image processing and storage. The pictures are processed and uploaded to Alibaba Cloud. For code reuse, we encapsulate the code logic related to image storage and provide a unified AliyunImageStore class for the entire system to use. The specific code implementation is as follows:


public class AliyunImageStore {
    
    
  //...省略属性、构造函数等...
  
  public void createBucketIfNotExisting(String bucketName) {
    
    
    // ...创建bucket代码逻辑...
    // ...失败会抛出异常..
  }
  
  public String generateAccessToken() {
    
    
    // ...根据accesskey/secrectkey等生成access token
  }
  
  public String uploadToAliyun(Image image, String bucketName, String accessToken) {
    
    
    //...上传图片到阿里云...
    //...返回图片存储在阿里云上的地址(url)...
  }
  
  public Image downloadFromAliyun(String url, String accessToken) {
    
    
    //...从阿里云下载图片...
  }
}

// AliyunImageStore类的使用举例
public class ImageProcessingJob {
    
    
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他无关代码...
  
  public void process() {
    
    
    Image image = ...; //处理图片,并封装为Image对象
    AliyunImageStore imageStore = new AliyunImageStore(/*省略参数*/);
    imageStore.createBucketIfNotExisting(BUCKET_NAME);
    String accessToken = imageStore.generateAccessToken();
    imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
  }
  
}

The entire upload process consists of three steps: creating a bucket (you can simply understand it as a storage directory), generating access token access credentials, and carrying the access token to upload pictures to the specified bucket. The code implementation is very simple, the several methods in the class are very cleanly defined, and they are very clear to use. At first glance, there is no big problem, and it can fully meet our business needs for storing pictures in Alibaba Cloud.

However, the only constant in software development is change. After a period of time, we built our own private cloud. We no longer store pictures on Alibaba Cloud, but store pictures on our self-built private cloud. In order to meet such a demand change, how do we modify the code?

We need to redesign and implement a PrivateImageStore class that stores images in a private cloud, and use it to replace all AliyunImageStore class objects in the project. Such a modification does not sound complicated, it is just a simple replacement, and the entire code is not changed much. However, we often say, "Details are the devil." This sentence is especially applicable in software development. In fact, the design implementation just now hides a lot of "devil details" that are prone to problems. Let's take a look at them.

What methods need to be designed and implemented for the new PrivateImageStore class to replace the AliyunImageStore class while minimizing code modification? This requires us to define and reimplement all the public methods defined in the AliyunImageStore class one by one in the PrivateImageStore class. There will be some problems in doing so. I have summarized the following two points.

First of all, some function names in the AliyunImageStore class expose implementation details, for example, uploadToAliyun() and downloadFromAliyun(). If the colleague who develops this function does not have interface awareness and abstract thinking, then this naming method that exposes implementation details is not surprising. After all, initially we only considered storing pictures on Alibaba Cloud. And we copied this method containing the word "aliyun" into the PrivateImageStore class, which is obviously inappropriate. If we rename the uploadToAliyun() and downloadFromAliyun() methods in the new class, it means that we have to modify all the code that uses these two methods in the project, and the amount of code modification may be very large.

Second, the process of storing pictures in Alibaba Cloud may not be exactly the same as the process of storing pictures in the private cloud. For example, in the process of uploading and downloading images of Alibaba Cloud, access tokens need to be produced, while private clouds do not require access tokens. On the one hand, the generateAccessToken() method defined in AliyunImageStore cannot be copied to PrivateImageStore; on the other hand, when we use AliyunImageStore to upload and download images, the code uses the generateAccessToken() method. If you want to change it to private cloud upload and download Process, these codes need to be adjusted.

How to solve these two problems? The fundamental way to solve this problem is to follow the principle of "based on interface rather than implementation programming" when writing code. Specifically, we need to do the following 3 points.

  1. The naming of the function cannot reveal any implementation details. For example, the uploadToAliyun() mentioned earlier does not meet the requirements, and the word aliyun should be removed and replaced with a more abstract naming method, such as upload().

  2. Encapsulate specific implementation details. For example, the special upload (or download) process related to Alibaba Cloud should not be exposed to the caller. We encapsulate the upload (or download) process and provide a method to package all upload (or download) details to the caller.

  3. The implementation class defines an abstract interface. The specific implementation classes all rely on a unified interface definition and follow a consistent upload function protocol. Users rely on interfaces, rather than specific implementation classes to program.

We follow this idea and refactor the code. The refactored code is as follows:


public interface ImageStore {
    
    
  String upload(Image image, String bucketName);
  Image download(String url);
}

public class AliyunImageStore implements ImageStore {
    
    
  //...省略属性、构造函数等...

  public String upload(Image image, String bucketName) {
    
    
    createBucketIfNotExisting(bucketName);
    String accessToken = generateAccessToken();
    //...上传图片到阿里云...
    //...返回图片在阿里云上的地址(url)...
  }

  public Image download(String url) {
    
    
    String accessToken = generateAccessToken();
    //...从阿里云下载图片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    
    
    // ...创建bucket...
    // ...失败会抛出异常..
  }

  private String generateAccessToken() {
    
    
    // ...根据accesskey/secrectkey等生成access token
  }
}

// 上传下载流程改变:私有云不需要支持access token
public class PrivateImageStore implements ImageStore  {
    
    
  public String upload(Image image, String bucketName) {
    
    
    createBucketIfNotExisting(bucketName);
    //...上传图片到私有云...
    //...返回图片的url...
  }

  public Image download(String url) {
    
    
    //...从私有云下载图片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    
    
    // ...创建bucket...
    // ...失败会抛出异常..
  }
}

// ImageStore的使用举例
public class ImageProcessingJob {
    
    
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他无关代码...
  
  public void process() {
    
    
    Image image = ...;//处理图片,并封装为Image对象
    ImageStore imageStore = new PrivateImageStore(...);
    imagestore.upload(image, BUCKET_NAME);
  }
}

In addition, many people hope to reverse the definition of the interface by implementing the class when defining the interface. Write the implementation class first, and then see which methods are in the implementation class and copy it into the interface definition. If you follow this way of thinking, it is possible that the interface definition is not abstract enough and depends on the specific implementation. Such interface design is meaningless. However, if you think this way of thinking is smoother, that's okay. Just move the methods of the implementation class to the interface definition selectively. Do not move the methods related to the specific implementation to the interface. , Such as the generateAccessToken() method in AliyunImageStore.

To sum up, when we are doing software development, we must have abstract consciousness, encapsulation consciousness, and interface consciousness. When defining the interface, do not expose any implementation details. The definition of the interface only indicates what to do, not how to do it. Moreover, when designing an interface, we have to think more about whether such an interface design is universal enough and whether it can be achieved when replacing a specific interface implementation without any interface definition changes.

Do I need to define an interface for each class?

After reading the explanation just now, you may have this question: In order to meet this principle, do I need to define a corresponding interface for each implementation class? When developing, should any code only rely on the interface and not on the implementation programming at all?

You must pay attention to a "degree" in everything you do. Overuse this principle and you have to define an interface for each class. Interfaces are flying all over the sky, which will also cause unnecessary development burden. As for when we should define an interface for a certain class and implement interface-based programming, when we do not need to define an interface and use the implementation class programming directly, the fundamental basis for us to make a trade-off is to return to the original intention of the birth of the design principle. As long as you figure out what kind of problem this principle is designed to solve, you will find that many previously ambiguous problems will suddenly become clear.

As we mentioned earlier, the original intention of this principle was to separate the interface and the implementation, encapsulate the unstable implementation, and expose the stable interface. The upstream system is interface-oriented rather than implementation programming, and does not rely on unstable implementation details, so that when the implementation changes, the code of the upstream system basically does not need to be changed, so as to reduce the coupling between the codes and improve the expansion of the code Sex.

From the point of view of the original design, if there is only one implementation of a function in our business scenario, and it cannot be replaced by other implementations in the future, then we do not need to design an interface for it, and there is no need to program based on the interface. , Just use the implementation class directly.

In addition, the more unstable the system, the more we have to work hard on code scalability and maintainability. On the contrary, if a system is extremely stable and basically does not need to be maintained after development, then we do not need to invest unnecessary development time for its scalability.

Key review

That's it for today's content. Let's summarize and review the key content you need to master.

  1. "Programming based on interface rather than implementation", another way of expressing this principle is "programming based on abstraction rather than implementation". The latter way of expression actually better reflects the original intention of this principle. When we are doing software development, we must have abstract consciousness, encapsulation consciousness, and interface consciousness. The more abstract, the higher the top level, and the more deviating from a specific implementation design, the more flexibility, scalability, and maintainability of the code can be improved.

  2. When we define an interface, on the one hand, the naming must be sufficiently general and cannot contain words related to the specific implementation; on the other hand, the methods related to the specific implementation should not be defined in the interface.

  3. The principle of "based on interface rather than implementation programming" can not only guide very detailed programming development, but also guide higher-level architecture design and system design. For example, the "interface" design between the server and the client, the "interface" design of the class library.

Guess you like

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