为什么项目中总喜欢封装工具类?——从图片上传理解"解耦"思想
前言
刚开始学习 Android 或 Java 开发时,我一直有一个疑问:
明明几行 Retrofit 就能上传图片,为什么很多公司的项目还要封装一个
ImageUploadUtil、UploadManager或ImageRepository?
后来接触的项目越来越多,才发现:
真正要封装的不是图片上传,而是"变化"。
图片上传只是一个例子,它背后体现的是软件开发中非常重要的思想——解耦(Decoupling)。
本文就以图片上传为例,聊聊什么是解耦、什么时候需要解耦,以及项目中常见的解耦方式。
一、什么是耦合?
在软件开发中,**耦合(Coupling)**表示模块之间的依赖程度。
举一个最简单的例子。
假设发布动态页面需要上传图片。
如果代码这样写:
public void publish() {
MultipartBody.Part part =
MultipartBody.Part.createFormData(
"file",
file.getName(),
RequestBody.create(file, MEDIA_TYPE)
);
api.upload(part)
.enqueue(new Callback<UploadBean>() {
@Override
public void onResponse(...) {
// 获取图片地址
// 发布动态
}
@Override
public void onFailure(...) {
}
});
}
此时:
PublishActivity
│
│
▼
Retrofit 上传接口
发布页面不仅负责页面逻辑,还负责:
- 图片压缩
- Multipart 创建
- Retrofit 请求
- 上传失败处理
- 获取图片 URL
可以看到:
页面和上传逻辑紧紧绑在了一起。
这就是耦合。
二、为什么高耦合不好?
很多人会说:
能运行不就行了吗?
小项目确实没问题。
但是项目一大,就开始出现各种问题。
例如:
项目有五个页面都要上传图片。
头像上传
发布动态
聊天发送图片
意见反馈
商品上传
每个页面都写一遍:
MultipartBody.Part.createFormData(...)
api.upload(...)
enqueue(...)
结果就是:
AvatarActivity
PublishActivity
FeedbackActivity
ChatActivity
GoodsActivity
里面都有几乎一样的上传代码。
以后服务器修改接口:
/upload
改成:
/api/v2/upload
你需要修改:
- AvatarActivity
- PublishActivity
- FeedbackActivity
- ChatActivity
- GoodsActivity
如果项目有二十多个页面,就意味着:
同一件事情需要修改二十多次。
这就是高耦合带来的维护成本。
三、什么是解耦?
解耦,就是:
降低模块之间的依赖,让每个模块只负责自己的事情。
例如,把上传代码抽出来。
PublishActivity
│
▼
UploadManager
│
▼
Retrofit
以后页面只需要:
uploadManager.upload(file);
页面根本不知道:
- Multipart 怎么创建
- Retrofit 怎么请求
- Token 怎么携带
- 图片怎么压缩
它只知道:
我要上传图片。
上传的所有细节,都交给 UploadManager。
这就是解耦。
四、为什么要解耦?
1、减少重复代码
以前:
Activity A
上传代码
Activity B
上传代码
Activity C
上传代码
现在:
Activity A
│
Activity B
│
Activity C
│
▼
UploadManager
所有页面共享同一套上传逻辑。
以后修改一次即可。
2、隐藏复杂逻辑
真正的上传,其实远没有想象中简单。
一个完整流程可能是:
选择图片
↓
检查格式
↓
检查大小
↓
图片压缩
↓
读取 EXIF
↓
修正旋转角度
↓
生成 RequestBody
↓
生成 Multipart
↓
上传服务器
↓
失败重试
↓
返回 URL
如果全部写到 Activity:
一个页面可能有几百行代码。
真正属于页面的代码:
点击按钮
刷新 UI
显示加载框
可能只有几十行。
其他全部都是上传逻辑。
这显然是不合理的。
所以:
Activity 负责页面。
UploadManager 负责上传。
职责更加明确。
3、方便修改
今天:
Retrofit 上传
明天:
公司改成:
阿里云 OSS
或者:
腾讯 COS
甚至:
MinIO
如果所有页面直接调用 Retrofit:
整个项目全部修改。
如果所有页面调用:
uploadManager.upload(file);
只需要修改 UploadManager 即可。
页面完全不用动。
4、统一处理异常
上传可能失败:
网络错误
Token 过期
服务器异常
图片太大
格式错误
如果每个页面自己处理:
整个项目的提示信息可能都不同。
例如:
页面 A:
Toast("上传失败")
页面 B:
Toast("网络异常")
页面 C:
Dialog("请稍后再试")
用户体验非常混乱。
如果统一放到 UploadManager:
所有上传异常都按照统一规范处理。
维护也更加方便。
五、什么时候应该解耦?
很多初学者有一个误区:
任何代码都应该封装。
其实不是。
解耦不是为了封装而封装。
而是当代码开始"变化"或者"重复"的时候,再考虑解耦。
通常有下面几种情况。
第一种:重复代码
例如:
三个页面都有:
api.login(...)
那么:
登录逻辑可以抽出来。
例如:
LoginRepository
第二种:一个类职责太多
例如:
MainActivity
里面:
- 登录
- 上传图片
- 下载文件
- 数据库操作
- 网络请求
- 权限申请
一个类几千行。
这种情况就应该拆分。
第三种:未来可能经常变化
例如:
支付。
今天:
微信支付
明天:
支付宝
后天:
银联
支付方式一直在变。
所以支付模块应该独立。
上传也是一样。
第四种:多个模块共同使用
例如:
图片加载。
如果:
首页
详情页
聊天
个人中心
都需要加载图片。
那么:
图片加载就应该统一封装。
例如:
ImageLoader
以后 Glide 换成 Coil。
只需要改一个地方。
六、常见的解耦方式
项目中最常见的几种方式如下。
① 工具类(Util)
适合:
没有状态。
纯工具方法。
例如:
DateUtil
StringUtil
MD5Util
② Manager
负责管理某一类业务。
例如:
UploadManager
DownloadManager
LocationManager
③ Repository
MVVM 中最常见。
负责数据来源。
例如:
UserRepository
ImageRepository
LoginRepository
Repository 不关心数据来自哪里。
可能来自:
网络
数据库
缓存
调用者完全不用关心。
④ 接口 + 实现
例如:
interface Upload {
void upload(File file);
}
实现:
OssUpload
CosUpload
LocalUpload
以后切换实现。
业务代码完全不用改。
这种方式也是大型项目最常见的解耦方式。
七、解耦不是目的,而是降低维护成本
很多人学习设计模式的时候,总想着:
我要把所有东西都抽出来。
其实这是错误的。
如果一个 Demo:
只有一个页面。
只有一次上传。
完全没必要:
UploadManager
UploadRepository
UploadFactory
UploadStrategy
UploadCallback
UploadHelper
这样反而增加了复杂度。
真正的原则应该是:
简单的代码保持简单,复杂的代码再进行解耦。
随着业务增长,再逐步拆分。
这也是很多成熟项目采用的演进方式。
总结
解耦并不是一种高深的技术,而是一种编程思想。
它的核心目标只有一个:
让每个模块只负责自己的事情,把容易变化的部分隔离出来,从而降低维护成本。
以图片上传为例,真正需要封装的不是 Retrofit,而是上传这一整套能力。
当上传逻辑集中到 UploadManager、Repository 等模块后,页面只需要关心"我要上传图片",而不用关心"图片是怎么上传的"。
写代码时,可以始终问自己两个问题:
- 这段代码会不会在多个地方重复出现?
- 这段代码以后会不会经常变化?
如果答案是"会",那么它就很可能值得解耦。
优秀的软件设计,不是把代码写得更复杂,而是把变化控制在最小的范围内。当一个需求变更时,只需要修改一个地方,而不是整个项目,这就是解耦真正的价值。