一、先说点啥
掘金前两天把我之前一篇关于 TypeScript使用面向对象开发 的文章推到了 掘金的官方公众号,引来评论区清一色的骂声哈哈哈。
我是理解的,毕竟看了很多代码,说实话,很糟糕。
今天的话题是,“你可能对封装和复用的思维有什么误解”。
首先,我想贴两段某 7.3K Stars 和 5.6K Starts 的后台管理类框架的代码:
1.1 片段一
export const getUsers = (params: any) =>
request({
url: '/users',
method: 'get',
params
})
export const getUserInfo = (data: any) =>
request({
url: '/users/info',
method: 'post',
data
})
export const deleteUser = (username: string) =>
request({
url: `/users/${username}`,
method: 'delete'
})
export const login = (data: any) =>
request({
url: '/users/login',
method: 'post',
data
})
export const logout = () =>
request({
url: '/users/logout',
method: 'post'
})
export const register = (data: any) =>
request({
url: '/users/register',
method: 'post',
data
})
1.2 片段二
// 获取用户列表
export const getUserList = (params: User.ReqUserParams) => {
return http.post<ResPage<User.ResUserList>>(PORT1 + `/user/list`, params);
};
// 获取树形用户列表
export const getUserTreeList = (params: User.ReqUserParams) => {
return http.post<ResPage<User.ResUserList>>(PORT1 + `/user/tree/list`, params);
};
// 新增用户
export const addUser = (params: { id: string }) => {
return http.post(PORT1 + `/user/add`, params);
};
// 批量添加用户
export const BatchAddUser = (params: FormData) => {
return http.post(PORT1 + `/user/import`, params);
};
// 编辑用户
export const editUser = (params: { id: string }) => {
return http.post(PORT1 + `/user/edit`, params);
};
// 删除用户
export const deleteUser = (params: { id: string[] }) => {
return http.post(PORT1 + `/user/delete`, params);
};
// 重置用户密码
export const resetUserPassWord = (params: { id: string }) => {
return http.post(PORT1 + `/user/rest_password`, params);
};
// 导出用户数据
export const exportUserInfo = (params: User.ReqUserParams) => {
return http.download(PORT1 + `/user/export`, params);
};
上面的代码有什么问题呢? 欢迎评论区讨论!
二、封装和复用
封装和复用的目的是什么?
封装的本意是,将相同或者相似的部分抽离到一起,实现代码的复用。
封装是个求同存异的过程。
封装有助于将一些固定的东西抽到一起,后续发生大变动的时候可以用最小的改动去应对。调用方无需做过多更改。
那么,封装的方式有哪些呢?
2.1 函数封装
这应该是大多数人对于封装的第一印象。
首先是,我们将公共的代码抽离到一起成为一个新的方法,然后暴露方法给调用方即可。
第一部分贴的代码至少是完成了函数部分的封装,至少这类代码大家都知道直接来这个方法库里找(我称之为方法库,应该又会有人喷了)
2.2 常量封装(魔法值提取)
所谓常量封装,只是借封装的思维来解释下魔法值。
何为魔法值?
此处省略八百个字。这就没必要再解释了吧?。。。
比如第一部分就出现了大量的魔法值,比如 user
users
add
edit
import
等等等等等等等等等等等等等等等。
再不济,这么写:
// constants.ts
export const USER = "user"
export const DELETE = "delete"
export const ADD = "add"
import {USER, DELETE} from './constants'
// 新增用户
export const addUser = (params: { id: string }) => {
return http.post(PORT1 + `/${USER}/${DELETE}`, params);
};
至少下次,后端说,我们想把所有的 add
方法改成 insert
的时候,你不会懵逼。
有人说,你们的后端怎么老瞎改?这里必须解释下了,并不是后端一定会改,而是编码习惯中为自己留后路,是以后不踩坑或者少踩坑的提前计划,让后续的自己不会被自己坑。另外,请记住,任何人都是不可信的,包括你们的后端。
2.3 方法的归类封装(或继承)
面向对象思维永远是封装离不开的一部分,恰好很多前端都没有:第一部分的代码就是。
大量的相似或者相同方法都是每个文件自己写自己的,都是增删改查,用户写一份、角色写一份、xxx写一份,大都是复制粘贴然后改改。
我在之前的文章中提过一句:“这是个拼爹的社会”,我在想为什么就不用继承来解决这些问题呢?
2.3.1 面向对象的继承
如果你使用的是 类的面向对象 编程,你可以这么写:
// BaseApi.ts
export abstract class AbstractBaseApi<T> {
// (Java: 卧槽你还能抽象属性???)
abstract baseUrl: string
async add(data: T): Promise<number> {
return await request(baseUrl + "/add", data)
}
async delete(id: number): Promise<number> {
return request(baseUrl + "/delete", {id})
}
// 其他方法
}
先抽离大家都会有的 增删改查,然后其他人就不需要再写,只需要继承:
// UserApi.ts
export class UserApi extends AbstractBaseApi<User> {
// 实现抽象属性,告诉父类API的目录
baseUrl = "user"
// 啥也不写,自动拥有 增删改查
async login(data: User): Promise<string> {
return request(baseUrl + "/login", data)
}
}
用户API啥也没写,就自带了父类的增删改查方法了,只需要写自己的一些其他特性方法,比如登录。
2.3.2 函数式封装Hook继承
如果你使用类似Hooks方式的函数继承封装,你可以这么写:
先写个基础的ApiHook,依然包含增删改查
// BaseApi.ts
export function useBaseApi<T>(baseUrl: string) {
async function add(data: T): Promise<number> {
return request(baseUrl + "/add", data)
}
async function delete(id: number): Promise<number> {
return request(baseUrl + "/delete", {id})
}
return {
add, delete
}
}
业务的ApiHook就可以直接使用这部分公共的方法:
// UserApi.ts
export function useUserApi() {
const baseUrl = "user"
// 把公共的方法拿过来 塞到自己的内部然后提供出去
const baseApi = useBaseApi<User>(baseUrl)
async function login(data: User): Promise<string> {
return request(baseUrl + "/login", data)
}
return {
...baseApi,
login
}
}
好,依然没有出现魔法值和重复代码。
3. 优雅且不过度封装
这里我们可能不会过度的去解释编码的一些 高内聚 低耦合 等概念,因为这些概念在很多文章中都有介绍。
且简单的说一些我们在日常代码中的一些规范吧。
3.1 尽可能没有魔法值
如果碰到同一个意义的数据出现 3 次以上,则必须抽离为一个常量。
比如一些共识,SECOND_PER_MINUTE_PER_HOUR = 60, 比如同一模块下的 URL(/user/add, /user/delete)的公共部分,比如同样含义的状态码等等等等等等等等
当然,不统一含义的反倒不允许抽离,因为这会增加代码的阅读难度:
// 错误示例 ❌
const ZERO = 0
// 使用1
if(result.code == ZERO)
// 使用2
if(list.length === ZERO)
// 正确示例 ✅
const SUCCESS = 0
// 使用1
if(result.code == SUCCESS)
// 使用2
if(list.length === 0)
3.2 尽可能少写相同或相似的代码
// 错误示例 ❌
class Notification {
static success(message: string, title: string): void {
ElNotification({
title: title,
message: message,
type: "success",
duration: 3000,
})
}
static error(message: string, title: string): void {
ElNotification({
title: title,
message: message,
type: "error",
duration: 3000,
})
}
// ...
}
// 正确示例 ✅
class Notification {
// 抽一个私有公共方法
private static show(message: string, title: string,type: string): void {
ElNotification({
title: title,
message: message,
type: type,
duration: 3000,
})
}
static error(message: string, title: string): void {
this.show(message, title, 'error')
}
static success(message: string, title: string): void {
this.show(message, title, 'success')
}
// ...
}
3.3 合理的使用继承
虽然继承可以解决很多公共代码重复的问题,但不合理的使用继承也会带来很多的困扰:
- 父类中存在大量使用率很低的方法
这种一般是新手使用继承,总觉得所有的方法都会被子类用到,于是大量的往父类中丢。
- 父类中的方法实现需要依赖大量的子类实现
这类一般是父类为抽象类,然后定义过多的规则需要子类实现,反倒为子类的使用带来更多的困扰。(这类一般建议父类使用模板方法的设计模式,能自己处理的一定自己先处理。)
抽到父类的判断标准之一,“如果子类继承后重写的成本高于不继承自己写的成本,那就别提到父类”
- 继承父类之后,还有其他方法还是需要大量的重复代码支持
这类问题一般是没有多继承语言导致的问题,建议使用其他委托类来处理这类问题。(后期会有文章介绍来仔细讲这类问题,如果你有兴趣,欢迎评论区留言)
四、思考和总结
像之前的文章中老被喷的一些观点一样,我还在坚持输出一些文章,观点和声音总是不断的,但思考和总结却从未停止过。
不管是使用什么样的编程范式、代码风格,写出来的代码都应该能看到自己在这段代码上的思考。
我们没有停止过,不管是在前端还是后端。
如果你对我的观点和文章感兴趣,欢迎你的点赞收藏,如果有疑问或者不同的观点,也欢迎评论区讨论。
我们的一些编程思维在下面两个开源项目中的代码中都有很多的体现:
感谢阅读,再见。