设计原则与思想: 面向对象(理论六)
为什么基于接口而非实现编程? 有必要为每个类都定义接口吗?
基于接口而非实现编程: 是一种非常有效的提高代码质量的手段.但是很容易被过度使用(比如每个类都定义接口)
一. 如何解读原则中"接口"二字?
基于接口而非实现编程: 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个步骤:
- 创建bucket(存储目录)
- 生成access token访问凭证
- 携带access token上传图片到指定的bucket中
总结:
乍一看没啥问题, 但是上面这段代码明显不能满足软件开发中的需求变化, 比如: 我们自建了私有云, 图片不存在阿里云了, 我们又该如何修改代码呢?
我们可能是再新建一个PrivateImageStore类, 并替换所有之前AliyunImageStore类的地方, 然后逐一实现AliyunImageStore类中的所有public方法. 这样做的话, 会存在2点问题, 如下:
- AliyunImageStore类中的方法命名暴露了实现细节, 比如: uploadToAliyun和downloadFromAliyun, 命名缺少了抽象思维, 那么我们就要调整所有用到这个方法的地方, 改动会很大.
- 图片存储在阿里云或私有云, 实现的步骤不一定是一致的. 比如: 阿里云上传和下载需要accessToken, 私有云可能会就不需要.
基于如上2个问题, 我们该如何解决呢?
在编写代码的时候, 我们需要遵从: 基于接口而非实现编程, 具体我们要做到以下3点:
- 函数命名不能暴露任何实现细节(比如: uploadToAliyun, downloadFromAliyun, 推荐: upload)
- 封装具体的实现细节(比如阿里云相关的, 上传和下载流程不应该暴露给调用者, 我们应进行封装, 对外包裹所有的上传和下载).
- 为实现类定义抽象的接口(具体的实现类都依赖统一的抽象接口定义, 使用者依赖接口, 而不是具体的实现类编程).
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 总结
我们在软件开发中, 一定要有抽象意识, 封装意识, 接口意识.
定义接口的时候, 不要暴露任何实现细节. 接口的定义只表明做什么, 而不是怎么做.
三. 是否需要为每个类定义接口?
凡事都要讲一个度, 过度的为每个类创建一个接口, 会导致接口满天飞, 增加不必要的开发负担.
基于接口而非实现编程: 封装不稳定的实现, 暴露出稳定的接口. 如果实现也是稳定(某个功能只有一种实现, 未来也不可能被其他实现方式替换)的话, 也就没必要使用接口了.