重构的艺术
重构的艺术🤪
强烈建议大家去看一下:
《重构:改善代码的既有设计》
这本书,超级好看,计算机圣经。🥳前半部分讲解为什么要进行重构、什么时候进行重构以及重构的好处,超级有意思;后半部分就是纯纯工具书,不知道咋重构的时候翻一翻就行💩
里面有句话我现在都记得:傻瓜都能写出计算机能看懂的代码,而能写出人能够看懂的代码的才叫优秀的程序员
一、为什么需要重构🤡
其实在计算机早期发展阶段
是没有重构的概念的,因为那个时候程序体量很小,所以推崇在开发之前就规划好一整个程序。
但是随着程序体量越来越大,即使是最优秀的大牛也不能在开发之前就把所有情况都考虑进去。👾
所以只能一次又一次地给原来的程序贴补丁,然而这种面对bug贴补丁的治标不治本的方式只会使补丁越来越多,程序越来越臃肿,越来越难以维护🫨
终于,到了一点都写不下去的时候,重构
的概念就诞生了,并且迅速获得了大家的青睐,并被认为是一种可以极大提高开发效率的方式。
在大家的
miniproject
中肯定也会遇到相似的问题:产品要求越来越多😵,一些原来根本没考虑的用法的产生,会导致代码逻辑越来越混乱,越写越自闭(去年我就在是这样的🤡🤡🤡)。这个时候,别害怕,捋清思路,果断重构!!!
但是,重构
的意义不止于此:随着技术的迅速发展,老一代技术渐渐被取代,这个时候,用新技术重构老项目
就显得格外重要
就比如用react
重构之前jQuery
的项目🤪
因此,小小总结一下,重构就是因为:
- 之前写的烂,改不了一点或者改的很耗时很费精力
- 项目要与时俱进,换上新技术,提高效能
二、要怎么重构🤕
《重构》的作者称这个为发现代码里的坏味道💩
首先,肯定是要捋清
逻辑混乱
产生的原因,分析过后,把逻辑重写,重新组织函数、组件的关系
一些逻辑混乱来源于不合理的组件拆分,不要想着让一个组件适应所有的情况,这样只会让这一个组件越来越难懂复杂
一个组件只干一件事情😃😃😃
就比如
button
组件,一开始的功能就是按下有反应。但是现在发现又需要一个可以有弹窗的button
组件。这时候一般的想法就是做个判断,给原来的组件上加一个
hasModal
,有这个属性就有弹窗,也不是不行嗷🤣。确实是这样,稍微加一点
prop
会让我们的组件功能更强大,但是过多的props
就挺让人头大了就比如再给这个
button
加上跳转页面、下载文件、上传文件等等的功能,这个时候浅浅看一下有多少个props
呢?hasModal
,navigatePath
,download
,upload
这个时候我就已经相当烦了,但是情况可能更超乎意料,可能还有
button
在页面上存在3秒之后页面就要开始抖动这种逆天的功能呢。你永远想象不到你的老板有什么鬼马点子😏所以,这个时候完全可以把这些个
button
拆开,正常的button
用一个组件,有特殊功能的,就单独再写一个,避免别人要这个组件的时候点开看到1000行的props提示
发呆。👾一些逻辑问题出现在组件之间
耦合度
太高,改动一个小部分要修改大半个页面的代码,这种是真的痛苦🤗还是拿上边的
button
做例子,hasModal
,navigatePath
,download
,upload
这些功能,完全可以改成一个onClick
回调函数😏,之后要用的时候把要执行的任务作为函数传进来,这样button
就只用单独处理自身onClick
的逻辑就行啦,复用性自然也就提高了一个好的组件写完之后用800年也不需要改一次代码
还有一些出在乱七八糟的逻辑条件判断上
if
嵌800行,图灵来了都看不懂,再来点循环,上帝都看不懂🦢好好
review
自己写的💩💩💩,把类似什么1
const True = flag ? 1 : 0
这种脱裤子放屁的东西都化简了(有时候
ide
都看不下去会给你标黄🤣);再把if
这种语句尽量改成三目运算符
;把switch
这种语句换成多态类
;提高点可读性,对你和你的
partner
都好🤯🤯🤯
其次就是重新组织文件结构
文件结构应该层次分明,逻辑清晰,以下是
husky🐶
推荐的目录结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27├── .husky # husky的配置文件
├── dist # 打包后的代码和资源文件
├── node_modules # 依赖库和模块
├── src
│ ├── assets # 静态资源
│ ├── components # 公共组件
│ ├── pages # 页面
│ ├── type # 全局类型声明
│ ├── utils # 工具函数
│ ├── App.tsx
│ ├── fetch.ts # 网络请求
│ ├── main.tsx
│ ├── router.tsx # 路由配置信息
│ └── vite-env.d.ts
├── .commitlintrc.cjs # commitlint的配置文件
├── .eslintignore
├── .eslintrc.cjs # ESLint的配置文件
├── .gitignore
├── .prettierignore
├── .prettierrc.cjs # Prettier的配置文件
├── index.html
├── package.json
├── README.md
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock其中,每个
page
和component
也都可以有自己独有的component
,最外层的component
只放不同页面不同组件均要用到的组件
然后就是一个被很多人忽略的点:换个好的名字
有时候,换一个人类能看懂的名字真的能帮上大忙😐。
不要怕名字太长什么的,又不是你自己写,
ide
都没抱怨你急什么?就比如说(真实案例哈😕):
你看到一个组件,叫
Ritem
,你能看出来它是干啥的吗?但是,如果改个名字,叫
RescueInfoItem
,是不是就清晰多了顺带提一句,前几天我起出了这辈子来最长的名字:
RescueInfoSortedByTargetIDSlice
😶🌫️
四、重构的步骤
光说说可能没什么感觉,接下来,我将会使用我最近重构的一段代码(js->ts)来给大家展示重构的魅力👽,用例子的方式给大家讲解一下重构的步骤(这段代码的来源保密🤪)
msgItem.jsx (这是我们要重构的组件)
1 |
|
这是一段超级长的代码,有110行,一般我们组件超过70行就已经不是很优雅了(反正我看到超过70行的组件就头大🥺),因此,重构就应该提上日程
那问题来了,该如何下手呢?🤨
要重构一个组件,首先要清除在哪里会被用到,我这里找了它的几个用法:
1
2
3
4
5
6
7// R_record.jsx
// 这里Ritem就是msgItem
{list?list.map((item)=>{
return (
<Ritem {...item} data={item} teacher={teacher} status={status} lap={lap} key='rescue' flag='record' />
)
}):<View className='img'>1
2
3
4
5
6// Main.jsx
{mine?mine.map((item)=>{
return (
<MsgItem {...item} data={item} ifmine key='item' flag='main' />
)
})1
2
3
4
5
6
7
8
9
10//Search.jsx
// 这段是之前我写的,一坨💩
{form &&
form.map((item) => <MsgItem
data={item}
{...item}
ifmine={form.some(itm => JSON.stringify(item) === JSON.stringify(itm))}
key='item'
flag='main'
></MsgItem>)}在真正重构的过程中,一定要理解这些个组件有什么用,是干啥的,但是现在这个例子中,先不用管,因为这个东西有点子复杂的,我自己都看半天😁,只知道它接受啥参数就可以了。
观察不难发现
参数中有个
flag
和key
,这俩名字一般是用来区分组件行为的,所以这个组件很可能会有两三个不同的功能{...item}
和data={item}
这俩玩意传的一摸一样,也可以删去还有一些
teacher
,status
等可传可不传的props
,这些说明组件功能可能有较大差异,可能之后要把它分为多个组件;或者把参数取消,传回调函数,完全受控组件降低耦合度
分析完之后,发现任务很艰巨啊,开摆得了(bushi)
之后,面对这样艰巨的任务,我一般是先挑软柿子捏,啥事软柿子嘞?
就是
名称
,目录结构
这些改了无伤大雅但是看着会很舒服的东西🐶《重构》的作者在书中说: 如果有一个人在重构的过程中有一段时间代码无法运行,那他肯定不在重构
因此,咱们重构要一步一步来。
从
名称
,目录结构
这些“皮毛”的东西开始,保证能正常运行的情况下,一点点慢慢改😝这里目录结构没啥问题,改个名字就行了,我给它起了个响当当的大名:
RerscueInfoItem
,之后都用RescueInfoItem
代替MsgItem
之后扫一眼代码,发现
1
const jiean = param.status==1?'未结案':'已结案'
1
backgroundColor:color==2?'#76A9FF':color==1?'#f9a53e':'#f57b70'
1
2
3
4
5
6const status = [
'未救援',
'救援中',
'已救援'
]
setFinish(status[res.data.rescue_target_info.status])这种东西,完全可以把它们搞成一个
Map
,再拿到外边的type
或config
文件里面嘛新开个文件咯,就叫
rescueMessageItemTypes
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// rescueMessageItemTypes.ts
export type rescueStatusType = {
text: string
color: string
}
export const rescueStatus: rescueStatusType[] = [
{
text: '未救援',
color: '#f57b70',
},
{
text: '救援中',
color: '#f9a53e',
},
{
text: '已救援',
color: '#76A9FF',
},
]结案这种一下子就搞定而且非常易懂的事情,就不单独拎出来做变量了,改完之后
return
里就变成这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<View className="inline">
<View
className="state"
style={{
backgroundColor:
rescueStatus[targetInfo ? targetInfo.status : 0].color,
}}
>
{rescueStatus[targetInfo ? targetInfo.status : 0].text}
</View>
<View className="rescued_level">风险等级:{props.risk_level}</View>
</View>
<View>
<View className="rescued_info">
<View className="row">
<View className="border_l"></View>微博内容:
</View>
<View className="demand">{props.text}</View>
</View>
<View className="rescued_city">地点:{props.area}</View>
</View>还是聚焦于表面功夫,作为
props
传进的参数一堆,而且要state
数量也不少,影响性能不说,反正是不好看1
2
3
4
5
6
7
8
9
10
11const [flag,setFlag]=useState('');
const [finish,setFinish]=useState('已救援')
// const [jiean,setJiean]=useState('')
const [color,setColor]=useState(0)//0:#f57b70 1:#76A9FF 2:#f9a53e
// const [status,setStatus]=useState(0)
const [text,setText]=useState('')
const [area,setArea]=useState('')
const [level,setLevel]=useState('')
const [data,setData]=useState([])
const [ifmine,setIfmine] = useState(false)但是这里很多
state
只是跟props
绑定,而props
不会经常改变,因此完全可以去掉这些state
,直接使用props
,优化性能。注意到跳转时传参很多,完全可以采用
redux
的方式,优化逻辑,把相似的变量和参数都提到外部:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// rescueInfoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import {
SingleRescueInfo,
RescueTargetInfoType,
RescueProcess,
} from '../Service/fetchTypes'
import { rescueInfoSliceType } from './sliceTypes'
const rescueInfoSlice = createSlice({
name: 'rescueInfo',
initialState: {
rescueInfo: {} as SingleRescueInfo,
targetInfo: {} as RescueTargetInfoType,
process: [],
eventID: 0,
targetID: 0,
} as rescueInfoSliceType,
reducers: {
updateRescueInfo: (
state: rescueInfoSliceType,
action: PayloadAction<SingleRescueInfo>,
) => ({
...state,
rescueInfo: action.payload,
eventID: action.payload.id,
targetID: action.payload.rescue_target_id,
}),
updateTargetInfo: (
state: rescueInfoSliceType,
action: PayloadAction<RescueTargetInfoType>,
) => ({ ...state, targetInfo: action.payload }),
updateProcess: (
state: rescueInfoSliceType,
action: PayloadAction<RescueProcess[]>,
) => ({ ...state, process: action.payload }),
},
})
export default rescueInfoSlice.reducer
export const { updateTargetInfo, updateRescueInfo, updateProcess } =
rescueInfoSlice.actions这里
redux
不理解不要紧,主要是理解抽出state
的这个思路,减少传参,提升可维护性😊现在,只需要一个变量:使用变量只需要从
store
中取出即可,也不需要另外传参给其余页面。同时,注意到
useEffect
中的调用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41useEffect(()=>{
/* console.log(param.flag) */
// setStatus(param.status)//主页item
if(param.flag=='main') //主页
{
setFlag('alarm')
if(param.ifmine)
{
setIfmine(true)
setFinish('救援中')
setColor(1)
}
else{
// 未认领的救援
setIfmine(false)
Fetch(
'/rescue/targetinfo',
{
"rescue_target_id": param.rescue_target_id,
},
'POST'
).then(
res=>{
//console.log(res)
setFinish(status[res.data.rescue_target_info.status])
setColor(res.data.rescue_target_info.status)
}
)
}}
else{
//record
setFlag('record')
setFinish(status[param.status])
setColor(param.status)
}
setText(param.text)
setArea(param.area)
setLevel(param.risk_level)
setData(param.data)
},[param])这些
setState
都可以省去,但是,很明显,这里逻辑很复杂, 需要重新组织一下逻辑这个时候,尘封已久的大脑就要开始转动了:
可以看到,主要是
flag
这个值是逻辑复杂的开始,根据注释可以知道,写的人当时也很挣扎🐶。这可能是因为后端接口的问题,导致主页面上有未认领的任务,需要重新请求才能得到信息。
然而猛然一看,看到这段,
1
2
3
4
5
6if(param.ifmine)
{
setIfmine(true)
setFinish('救援中')
setColor(1)
}也没在干什么,所以就索性一视同仁,都发个请求呗
这个时候,我是喜欢把请求这些东西放到外边去的,搞个
service
文件夹咯1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85// /service/fetchType.ts
export type BaseResType<T> = {
code: number
msg: string
data: T
}
export type requestType = 'GET' | 'POST' | 'PUT' | 'DELETE'
export interface RescueTargetInfoResponse {
rescue_target_info: RescueTargetInfoType
}
// 这段直接从apifox上copy就行
export interface RescueTargetInfoType {
create_time: string
/**
* 救援过程描述
*/
description: string
/**
* 救援结束时间
*/
end_time: string
/**
* 最终评价
*/
evalutaion: string
/**
* 救援对象id
*/
id: number
/**
* 救援对象昵称(按最新的救援信息数据)
*/
nickname: string
/**
* 救援老师1id
*/
rescue_teacher1_id: number
/**
* 救援老师1姓名
*/
rescue_teacher1_name: string
/**
* 救援老师1身份
*/
rescue_teacher1_role: number
/**
* 救援老师2id
*/
rescue_teacher2_id: number
/**
* 救援老师2姓名
*/
rescue_teacher2_name: string
/**
* 救援老师2身份
*/
rescue_teacher2_role: number
/**
* 救援老师3id
*/
rescue_teacher3_id: number
/**
* 救援老师3姓名
*/
rescue_teacher3_name: string
/**
* 救援老师3身份
*/
rescue_teacher3_role: number
/**
* 救援起始时间
*/
start_time: string
/**
* 救援状态(0-待救援 1-救援中 2-已救援)
*/
status: number
update_time: string
/**
* 救援对象微博地址(唯一标识)
*/
weibo_address: string
}1
2
3
4
5
6
7
8
9
10
11// /service/rescueInfoByID.ts
export const FetchRescueTargetInfo = async (targetID: number) => {
const data = {
rescue_target_id: targetID,
}
return Fetch<BaseResType<RescueTargetInfoResponse>>(
'/rescue/targetinfo',
data,
'POST',
)
}根据它传进来的
props
,来请求target_info
,并且变成state
放起来这个时候,文件已经开始变得清爽了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51const MsgItem=(props)=>{
const { navURL, ...restProps } = props
const [targetInfo, setTargetInfo] = useState<RescueTargetInfoType>()
useEffect(() => {
FetchRescueTargetInfo(restProps.rescue_target_id).then((res) => {
setTargetInfo(res.data.rescue_target_info)
})
}, [])
function toR_detail()
{
console.log(flag)
if(flag=='alarm')
Taro.navigateTo({
url:`/moduleA/pages/Alarm/index?ifmine=${ifmine}&data=${JSON.stringify(data)}&finish=${finish}&color=${color}`
})
else{
console.log('todetail')
const jiean = param.status==1?'未结案':'已结案'
Taro.navigateTo({
url:`/moduleA/pages/Rescue_detail/index?finish=${jiean}&data=${JSON.stringify(data)}&teacher=${JSON.stringify(param.teacher)}&lap=${JSON.stringify(param.lap)}`
})
}
}
return(
<View className='noti-item column' onClick={toR_detail} >
<View className="inline">
<View
className="state"
style={{
backgroundColor:
rescueStatus[targetInfo ? targetInfo.status : 0].color,
}}
>
{rescueStatus[targetInfo ? targetInfo.status : 0].text}
</View>
<View className="rescued_level">风险等级:{props.risk_level}</View>
</View>
<View>
<View className="rescued_info">
<View className="row">
<View className="border_l"></View>微博内容:
</View>
<View className="demand">{props.text}</View>
</View>
<View className="rescued_city">地点:{props.area}</View>
</View>
</View>
)
}接下来,就剩一个
toR_detail
函数要改咯,也是撒撒水啦,因为刚刚我们把state都放到外边去了,这里也不用通过传参来让其他页面访问咯,超级爽,略微改改,就这样咯,其中Nav
是我封装的跳转函数,因为不想写那么多Taro.navigateTo({url:"......"})
:1
2
3
4
5
6function Navi() {
const navURL_real = navURL || '/moduleA/pages/Alarm/index'
store.dispatch(updateRescueInfo(restProps))
targetInfo && store.dispatch(updateTargetInfo(targetInfo))
Nav(navURL_real)
}这下就大功告成了,吗?还记得一开始的那些组件传的
props
吗?也该清清啦!,像什么flag
,ifmine
,都可以扔掉啦,只留下基本的msg信息就行1
2
3// R_record.jsx
// 这里Ritem就是msgItem
{list?list.map((item)=><Ritem {...item} />):<View className='img'>1
2// Main.jsx
{mine?mine.map((item)=><MsgItem {...item}/>)1
2
3//Search.jsx
// 这段是之前我写的,一坨💩
{form && form.map((item) => <MsgItem {...item} />)}现在回头看看我们重构后的组件的全貌吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60import { View } from '@tarojs/components'
import React, { useEffect, useState } from 'react'
import './index.less'
import {
RescueMessageItemProps,
rescueStatus,
} from '../types/rescueMessageItemTypes'
import { Nav } from '../../utils/taroFunctions'
import { FetchRescueTargetInfo } from '../../Service/rescueInfoByID'
import { RescueTargetInfoType } from '../../Service/fetchTypes'
import {
updateRescueInfo,
updateTargetInfo,
} from '../../slices/rescueInfoSlice'
import store from '../../store/store'
const RescueMessageItem: React.FC<RescueMessageItemProps> = (props) => {
const { navURL, ...restProps } = props
const [targetInfo, setTargetInfo] = useState<RescueTargetInfoType>()
useEffect(() => {
FetchRescueTargetInfo(restProps.rescue_target_id).then((res) => {
setTargetInfo(res.data.rescue_target_info)
})
}, [])
function Navi() {
const navURL_real = navURL || '/moduleA/pages/Alarm/index'
store.dispatch(updateRescueInfo(restProps))
targetInfo && store.dispatch(updateTargetInfo(targetInfo))
Nav(navURL_real)
}
return (
<View className="noti-item column" onClick={Navi}>
<View className="inline">
<View
className="state"
style={{
backgroundColor:
rescueStatus[targetInfo ? targetInfo.status : 0].color,
}}
>
{rescueStatus[targetInfo ? targetInfo.status : 0].text}
</View>
<View className="rescued_level">风险等级:{props.risk_level}</View>
</View>
<View>
<View className="rescued_info">
<View className="row">
<View className="border_l"></View>微博内容:
</View>
<View className="demand">{props.text}</View>
</View>
<View className="rescued_city">地点:{props.area}</View>
</View>
</View>
)
}
export default RescueMessageItem一共是59行,是不是特别的一目了然,看到之后,心情都变好了,不枉我重构这么久🤗
五、总结⬇️
在《重构》
中,作者提到了营地法则,即要保证在你来之后,代码变得更好而不是更糟糕,而保证代码变得更好的秘诀,就是重构。这次通过一个小例子,来让大家知道把时间花在重构上并不可怕,经常重构并不见得是一件坏事,相反的,经常重构代表着你在积极思考,努力让代码变得更优秀;而不重构,只能是给未来的自己和同伴添堵咯。当然,过早过频繁的重构也是不提倡的,就比如:本来这个按钮只用负责跳转,你写着写着,突然想到以后要是要让他能点一下出来一个ggbond
怎么办,你库库开始重构,重构完之后,一天就过去了。然而产品老板到最后也没有点一下出来一个ggbond
的需求(虽然这真的很酷😝)。那这一天就是白费的。总之,重构就是一门艺术,过多过少都不提倡,要根据项目要求,灵活重构。不多说了,这些写多了自然就有感觉啦🤗