设计原则与思想: 面向对象(理论六)

为什么基于接口而非实现编程? 有必要为每个类都定义接口吗?

基于接口而非实现编程: 是一种非常有效的提高代码质量的手段.但是很容易被过度使用(比如每个类都定义接口)

一. 如何解读原则中"接口"二字?

基于接口而非实现编程: Program to an interface, not an implementation

它诞生于1994年GOF书中, 先于很多编程语言(如java), 是一条比较抽象, 泛化的设计思想.

本质上来看, 接口就是一组协议或者约定, 是给使用者提供的一个功能列表.接口和实现相分离, 上游系统面向接口而非实现编程, 不依赖不稳定的实现细节, 当实现发生变化时, 上游代码不需要改动, 降低耦合, 提供扩展性.

基于接口而非实现编程的另一种表述方式: 基于抽象而非实现编程. 后者更能体现这条原则设计的初衷. 在软件开发世界中, 最大的挑战之一就是需求不断的变化, 这也是考验代码好坏的一个标准.

越抽象, 越顶层, 越脱离具体的某一实现的设计, 越能提高代码的灵活性, 越能应对未来的需求变化.而抽象就是提高代码的扩展性, 灵活性, 可维护性的最有效手段之一.

二. 如何将这条原则应用到实战中?

2.1 图片处理和存储的Demo

场景: 我们系统涉及图片处理和存储, 图片经过处理后, 上传到阿里云.

新建一个统一的AliyunImageStore类, 负责图片的处理

    package org.example.case_04;  
  
import java.awt.*;  
  
/**  
* @Author: Nisy  
* @Date: 2023/06/13/21:15  
* @desc: 通用的图片处理和存储  
*/  
public class AliyunImageStore {  
  
/**  
* 创建存储目录  
* @param bucketName  
*/  
public void createBucketIfNotExisting(String bucketName){  
//TODO 创建Bucket代码  
}  
  
/**  
* 生成access token 访问凭证  
* @return  
*/  
public String generateAccessToken(){  
//TODO 根据access key | secret key等生成access token  
return null;  
}  
  
  
/**  
* 上传图片到阿里云服务器  
* @param image  
* @param bucketName 存储目录  
* @param accessToken 访问凭证  
* @return 阿里云图片地址  
*/  
public String uploadToAliyun(Image image, String bucketName, String accessToken){  
return null;  
}  
  
/**  
* 下载图片从阿里云服务器  
* @param url 阿里云图片地址  
* @param accessToken 访问凭证  
* @return  
*/  
public Image downloadFromAliyun(String url, String accessToken){  
return null;  
}  
}

应用:

package org.example.case_04;  
  
import java.awt.*;  
  
/**  
* 例子  
* @Author: Nisy  
* @Date: 2023/06/13/21:22  
*/  
public class ImageProcessJob {  
  
private static final String BUCKET_NAME = "ai_images_bucket";  
  
public void process(){  
//处理图片, 并封装成Image对象  
Image image = null;  
AliyunImageStore imageStore = new AliyunImageStore();  
  
//创建存储目录  
imageStore.createBucketIfNotExisting(BUCKET_NAME);  
//获取访问凭证  
String accessToken = imageStore.generateAccessToken();  
//上传图片到阿里云服务器  
String url = imageStore.uploadToAliyun(image, BUCKET_NAME, accessToken);  
}  
  
}

上传流程一共3个步骤:

  1. 创建bucket(存储目录)
  2. 生成access token访问凭证
  3. 携带access token上传图片到指定的bucket中

总结:

乍一看没啥问题, 但是上面这段代码明显不能满足软件开发中的需求变化, 比如: 我们自建了私有云, 图片不存在阿里云了, 我们又该如何修改代码呢?

我们可能是再新建一个PrivateImageStore类, 并替换所有之前AliyunImageStore类的地方, 然后逐一实现AliyunImageStore类中的所有public方法. 这样做的话, 会存在2点问题, 如下:

  1. AliyunImageStore类中的方法命名暴露了实现细节, 比如: uploadToAliyun和downloadFromAliyun, 命名缺少了抽象思维, 那么我们就要调整所有用到这个方法的地方, 改动会很大.
  2. 图片存储在阿里云或私有云, 实现的步骤不一定是一致的. 比如: 阿里云上传和下载需要accessToken, 私有云可能会就不需要.

基于如上2个问题, 我们该如何解决呢?

在编写代码的时候, 我们需要遵从: 基于接口而非实现编程, 具体我们要做到以下3点:

  1. 函数命名不能暴露任何实现细节(比如: uploadToAliyun, downloadFromAliyun, 推荐: upload)
  2. 封装具体的实现细节(比如阿里云相关的, 上传和下载流程不应该暴露给调用者, 我们应进行封装, 对外包裹所有的上传和下载).
  3. 为实现类定义抽象的接口(具体的实现类都依赖统一的抽象接口定义, 使用者依赖接口, 而不是具体的实现类编程).

2.2 优化后的图片上传和下载Demo

抽象出ImageStore接口

package org.example.case_05;  
  
import java.awt.*;  
  
/**  
* 抽象图片存储接口  
* @Author: Nisy  
* @Date: 2023/06/13/21:46  
*/  
public interface ImageStore {  
  
/**  
* 上传  
* @param image  
* @param bucketName  
* @return  
*/  
String upload(Image image, String bucketName);  
  
  
/**  
* 下载  
* @param url  
* @return  
*/  
Image downlaod(String url);  
  
  
/**  
* 创建存储目录  
* @param bucketName  
*/  
default void createBucketIfNotExisting(String bucketName){  
//TODO 创建存储目录  
}  
}

阿里云图片存储类, 实现ImageStore接口

package org.example.case_05;  
  
import java.awt.*;  
  
/**  
* 阿里云图片存储  
* @Author: Nisy  
* @Date: 2023/06/13/21:47  
*/  
public class AliyunImageStore implements ImageStore{  
  
/**  
* 上传  
* @param image  
* @param bucketName  
* @return  
*/  
@Override  
public String upload(Image image, String bucketName) {  
this.createBucketIfNotExisting(bucketName);  
String accessToken = generateAccessToken();  
//TODO 上传图片到阿里云  
return null;  
}  
  
  
/**  
* 下载  
* @param url  
* @return  
*/  
@Override  
public Image downlaod(String url) {  
String accessToken = this.generateAccessToken();  
// TODO 从阿里云下载图片  
return null;  
}  
  
  
/**  
* 生成access token  
* @return  
*/  
private String generateAccessToken(){  
return null;  
}  
  
}

私有云图片存储类, 实现ImageStore接口


package org.example.case_05;  
  
import java.awt.*;  
  
/**  
* 私有云图片存储(无需accss token)  
* @Author: Nisy  
* @Date: 2023/06/13/21:51  
*/  
public class PrivateImageStore implements ImageStore{  
  
  
/**  
* 上传  
* @param image  
* @param bucketName  
* @return  
*/  
@Override  
public String upload(Image image, String bucketName) {  
this.createBucketIfNotExisting(bucketName);  
//TODO 上传图片到私有云  
return null;  
}  
  
  
  
/**  
* 下载  
* @param url  
* @return  
*/  
@Override  
public Image downlaod(String url) {  
//从私有云下载图片  
return null;  
}  
  
}

应用

package org.example.case_05;  
  
import java.awt.*;  
  
/**  
* @Author: Nisy  
* @Date: 2023/06/13/21:55  
*/  
public class ImageProcessJob {  
  
private static final String BUCKET_NAME = "ai_images_bucket";  
  
public void process(){  
//处理图片, 并封装Image对象  
Image image = null;  
ImageStore imageStore = new PrivateImageStore();  
imageStore.upload(image, BUCKET_NAME);  
}  
  
}

2.3 总结

我们在软件开发中, 一定要有抽象意识, 封装意识, 接口意识.

定义接口的时候, 不要暴露任何实现细节. 接口的定义只表明做什么, 而不是怎么做.

三. 是否需要为每个类定义接口?

凡事都要讲一个度, 过度的为每个类创建一个接口, 会导致接口满天飞, 增加不必要的开发负担.

基于接口而非实现编程: 封装不稳定的实现, 暴露出稳定的接口. 如果实现也是稳定(某个功能只有一种实现, 未来也不可能被其他实现方式替换)的话, 也就没必要使用接口了.

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务