Android:你还在等那个,手把手带你重构的人出现吗?

前言

高能预警:本文有一点长,建议收藏后再看。

以下你就可以看到,一位单枪匹马的帅哥,是如何以一己之力,重构整座“屎山”的。

这位帅哥一直在徘徊,本文到底该写给谁看?是只在乎写功能的码农吗?不了不了,码农若真的有心提升代码质量,就不会在项目中丧心病狂的堆积屎山。

于是干脆写写重构心得、分享重构思路,让那些有意识在这方面有所提升的帅哥美女们,少走些弯路!

在此首先感谢主管的信任与支持。本次重构中,帅哥在部门内部兜售并率先使用某架构,5 天内完成 60 个类的核心模块的重构。(不要慌,架构已在 GitHub 开源,文末链接给出。)

以下正文。

代码是如何越写越烂的?

你是否经常听同事自嘲,“开始还想好好写,不知怎滴,后面越写越烂”?

代码越写越烂,果真是个没有端倪、无法干预的魔咒玄学吗?

让我们来快速浏览一下 重构前 项目里的代码是怎么写的。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
protected void initView() {
PagerAdapter pagerAdapter = new PagerAdapter();
viewPagerFix.setOffscreenPageLimit(4);
viewPagerFix.setAdapter(pagerAdapter);
mFragmentBinding.tabLayout.setTabData(pagerAdapter.titles);
mFragmentBinding.tabLayout.setOnTabSelectListener(new OnTabSelectListener() {
@Override
public void onTabSelect(int position) {
viewPagerFix.setCurrentItem(position);
}

@Override
public void onTabReselect(int position) {

}
});
viewPagerFix.addOnPageChangeListener(new ViewPagerFix.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
KeyboardUtils.hideSoftInput(getActivity());
}

@Override
public void onPageSelected(int position) {
mFragmentBinding.tabLayout.setCurrentTab(position);
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {

zzbgPageSelected(position);

} else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) {
switch (position) {
case 0:
case 1:
mViewModel.removeAllArrows();

if (mAttachmentFragment != null) {
mAttachmentFragment.hideClickHighLight(ALBUM_ALL);
}
break;
case 2:
if (mAttachmentFragment != null) {
mAttachmentFragment.initAttachTitle();
}
mViewModel.showAllArrows();

break;
default:
break;
}
} else {
switch (position) {
case 0:
case 1:
case 2:
mViewModel.removeAllArrows();
//hideBottomLayout();
if (mAttachmentFragment != null) {
mAttachmentFragment.hideClickHighLight(ALBUM_ALL);
}
break;
case 3:
if (mAttachmentFragment != null) {
mAttachmentFragment.initAttachTitle();
}
mViewModel.showAllArrows();

break;
default:
break;
}
}
}

@Override
public void onPageScrollStateChanged(int state) {

}
});
viewPagerFix.setCurrentItem(0);
mFragmentBinding.headContainer.getTitleView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {
return;
}

mViewModel.changeWyhcrwMajorState();
EventBus.getDefault().post(new RefreshItemEventBus(
mViewModel.getXXXDetailTouchManager().getCurrentWyhcrw()));
}
});
}

private void zzbgPageSelected(int position) {

if (mScreenNum == 3) {

switch (position) {
case 0:
case 1:
mViewModel.removeAllArrows();

if (mAttachmentFragment != null) {
mAttachmentFragment.hideClickHighLight(ALBUM_ALL);
}
break;
case 2:
mViewModel.showAllArrows();

break;
default:
break;
}

} else {

switch (position) {
case 0:
mViewModel.removeAllArrows();

if (mAttachmentFragment != null) {
mAttachmentFragment.hideClickHighLight(ALBUM_ALL);
}
break;
case 1:
mViewModel.showAllArrows();

break;
default:
break;
}

}
;

}



/**
* viewPager适配器
*/
private class PagerAdapter extends FragmentPagerAdapter {

String[] titles;

PagerAdapter() {
super(getChildFragmentManager());
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {

if (mScreenNum == 3) {

titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_no_tbjt);

} else {
titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_zzbg);

}

} else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) {
titles = getResources().getStringArray(R.array.XXX_detail_tabs_for_ybjz);
} else {
titles = getResources().getStringArray(R.array.XXX_detail_tabs);
}
}

@Override
public Fragment getItem(int position) {
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {

return zzbgGetItem(position);

} else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) {
switch (position) {
case 0:
if (mXXXTuBanPicFragment == null) {
mXXXTuBanPicFragment = XXXTuBanPicFragment.newInstance(
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger()
);
}
return mXXXTuBanPicFragment;
case 1:
if (mRecordFragment == null) {
mRecordFragment = XXXRecordFragment.newInstance(mViewModel.getXXXDetailTouchManager());
}
return mRecordFragment;

default:
if (mAttachmentFragment == null) {
mAttachmentFragment = XXXAttachmentFragment.newInstance(
mViewModel.getAttachments(),
mViewModel.getOriginalAttachments(),
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger(),
XXXDetailFragment.this
);
}
return mAttachmentFragment;
}
} else {
switch (position) {
case 0:
if (mXXXTuBanPicFragment == null) {
mXXXTuBanPicFragment = XXXTuBanPicFragment.newInstance(
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger()
);
}
return mXXXTuBanPicFragment;
case 1:
if (mAttributeFragment == null) {
mAttributeFragment = XXXAttributeFragment.newInstance(
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger()
);
}
return mAttributeFragment;

case 2:
if (mRecordFragment == null) {
mRecordFragment = XXXRecordFragment.newInstance(mViewModel.getXXXDetailTouchManager());
}
return mRecordFragment;
default:
if (mAttachmentFragment == null) {
mAttachmentFragment = XXXAttachmentFragment.newInstance(
mViewModel.getAttachments(),
mViewModel.getOriginalAttachments(),
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger(),
XXXDetailFragment.this
);
}
return mAttachmentFragment;
}
}
}

private Fragment zzbgGetItem(int position) {

if (mScreenNum == 3) {

switch (position) {
case 0:
if (mAttributeFragment == null) {
mAttributeFragment = XXXAttributeFragment.newInstance(
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger()
);
}
return mAttributeFragment;
case 1:
if (mRecordFragment == null) {
mRecordFragment = XXXRecordFragment.newInstance(
mViewModel.getXXXDetailTouchManager());
}
return mRecordFragment;
default:
if (mAttachmentFragment == null) {
mAttachmentFragment = XXXAttachmentFragment.newInstance(
mViewModel.getAttachments(),
mViewModel.getOriginalAttachments(),
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger(),
XXXDetailFragment.this
);
}
return mAttachmentFragment;
}

} else {
switch (position) {
case 0:
if (mRecordFragment == null) {
mRecordFragment = XXXRecordFragment.newInstance(
mViewModel.getXXXDetailTouchManager());
}
return mRecordFragment;
default:
if (mAttachmentFragment == null) {
mAttachmentFragment = XXXAttachmentFragment.newInstance(
mViewModel.getAttachments(),
mViewModel.getOriginalAttachments(),
mViewModel.getUniqueCode(),
mViewModel.getXXXTouchManger(),
XXXDetailFragment.this
);
}
return mAttachmentFragment;
}
}

}

@Override
public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {
if (mScreenNum == 3) {

switch (position) {
case 0:
mAttributeFragment = (XXXAttributeFragment) object;
break;
case 1:
mRecordFragment = (XXXRecordFragment) object;
break;
default:
mAttachmentFragment = (XXXAttachmentFragment) object;
break;
}

} else {
switch (position) {
case 0:
mRecordFragment = (XXXRecordFragment) object;
break;
default:
mAttachmentFragment = (XXXAttachmentFragment) object;
break;
}
}

return object;
} else if (mViewModel.getXXXDetailTouchManager().isYBJZ()) {
switch (position) {
case 0:
mXXXTuBanPicFragment = (XXXTuBanPicFragment) object;
break;
case 1:
mRecordFragment = (XXXRecordFragment) object;
break;
default:
mAttachmentFragment = (XXXAttachmentFragment) object;
break;
}
return object;
} else {
switch (position) {
case 0:
mXXXTuBanPicFragment = (XXXTuBanPicFragment) object;
break;
case 1:
mAttributeFragment = (XXXAttributeFragment) object;
break;
case 2:
mRecordFragment = (XXXRecordFragment) object;
break;
default:
mAttachmentFragment = (XXXAttachmentFragment) object;
break;
}
return object;
}
}

@Override
public int getCount() {
if (mViewModel != null) {
if (mViewModel.getXXXDetailTouchManager().isZZBG()) {
if (mScreenNum == 3) {
return 3;
}
return 2;
}
if (mViewModel.getXXXDetailTouchManager().isYBJZ()) {
return 3;
} else {
return 4;
}
}
return 0;
}

}

(为保护隐私,模块类名已替换为“XXX”)

可以看到,该主页目前服务于 3 个地区,每个地区对子页面的展示都有定制需求。

if else switch if else switch,只在乎功能实现的码农就是这么写的。

一个地区 50 行,那要是 10 个地区呢?公司领导放话要支持全国 100 个乡镇地区!那 100 个地区呢???

抽象,顺应的是“开闭原则”

这是一帮对“抽象”无感的码农。

他们听到“抽象”,就像不爱锻炼的我听到父母、朋友劝我“健身”一样被动。(笑)

正如我并不真的理解健身的意义所在,他们也当抽象是“耳边风”。

“100 个地区”这种,天然的就是用工厂模式来抽象和定制,这原本是一目了然、毫无疑问的事。

重构后的代码,主页抬头特意标注了警告。

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
/
* 友情提示:本类涂有防腐药品,切勿触碰,切勿触碰,切勿触碰!
* <p>
* 地区定制功能,包括特色的布局等,请继承于 AbstractDetailChildFragmentManager 单独编写!
*/

public class XXXDetailFragment extends BaseFragment implements IResponse {
protected void initView() {
initViewPagerManager();
PagerAdapter pagerAdapter = new PagerAdapter();
viewPagerFix.setOffscreenPageLimit(4);
viewPagerFix.setAdapter(pagerAdapter);
mFragmentBinding.tabLayout.setTabData(pagerAdapter.titles);
mFragmentBinding.tabLayout.setOnTabSelectListener(new OnTabSelectListener() {
@Override
public void onTabSelect(int position) {
viewPagerFix.setCurrentItem(position);
}

@Override
public void onTabReselect(int position) {

}
});
viewPagerFix.addOnPageChangeListener(new ViewPagerFix.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
KeyboardUtils.hideSoftInput(getActivity());
}

@Override
public void onPageSelected(int position) {
mFragmentBinding.tabLayout.setCurrentTab(position);
mDetailChildFragmentManager.onPageSelected(position);
}

@Override
public void onPageScrollStateChanged(int state) {

}
});
}

/**
* viewPager适配器
*/
private class PagerAdapter extends FragmentPagerAdapter {

String[] titles;

PagerAdapter() {
super(getChildFragmentManager());
titles = mDetailChildFragmentManager.getTitles();
}

@Override
public Fragment getItem(int position) {
return mDetailChildFragmentManager.getItem(position);
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
return mDetailChildFragmentManager.instantiateItem(container, position, object);
}

@Override
public int getCount() {
return mDetailChildFragmentManager.getCount();
}
}
}

代码是如何剪不断理还乱的?

听说过“代码耦合”和“解耦”的人很多,但真正理解这是怎么一回事的,恐怕只有你 ~

因为哪怕你不知,你也即将见证一位帅哥如何手把手带你解耦 ~

我们先来看下重构前的代码!

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
public interface XXXListNavigator {

void updateRecyclerView();

void showProgressDialog();

void dismissProgressDialog();

void updateListView();

void updateLayerWrapperList(List<LayerWrapper> list);

boolean isAnimationFinish();

void resetCount();

}

public class XXXListViewModel extends BaseViewModel {
public void multiAddOrRemove(ArrayList<String> bsms, boolean isAdd) {
if (null != mNavigator) {
mNavigator.showProgressDialog();
}
if (null == mMultiAddOrRemoveUseCase) {
mMultiAddOrRemoveUseCase = new MultiAddOrRemoveUseCase();
}
mUseCaseHandler.execute(mMultiAddOrRemoveUseCase, new MultiAddOrRemoveUseCase.RequestValues(isAdd, bsms,
mLayerWrapperObservableField.get()),
new UseCase.UseCaseCallback<MultiAddOrRemoveUseCase.ResponseValue>() {
@Override
public void onSuccess(MultiAddOrRemoveUseCase.ResponseValue response) {
ToastUtils.showShort(getApplicationContext(), "操作成功");
clearData();
loadData(true, true);
if (null != mNavigator) {
mNavigator.dismissProgressDialog();
}
}

@Override
public void onError() {
ToastUtils.showShort(getApplicationContext(), "操作失败");
if (null != mNavigator) {
mNavigator.dismissProgressDialog();
}
}
});
}
}

可以看到,UI 过度暴露了“处理 UI 逻辑所依赖的过程 API”,并在业务中直接干预了 UI 逻辑,这是典型的 MVP 写法,这造成了耦合。一旦 UI 的需求有变动,View 和 Presenter 的编写者都会受到牵连。

而且,职责过多造成了依赖过多,这个 Presenter 会因为过多的依赖,而越写越臃肿:受“破窗效应”的驱使,别的码农会因为此处已经有某个依赖,而不假思索的接着往下写。

到底怎样才算解耦

所谓解耦,是符合工程设计、符合设计模式原则的编码。

解耦的本质,我只说一遍:

职责边界明确,职责边界明确,职责边界明确。

viabus_flow_cn.png

符合单一职责原则:
UI 的职责仅限于“展示”,也就是发送请求、处理 UI 逻辑。业务的职责仅限于“提供数据”,也就是接收请求、处理业务逻辑、响应结果数据。

符合依赖倒置原则、最小知识原则:
UI 不需要知道数据是经过怎样的周转得来的,它只需发送请求,并在拿到结果数据后,自己内部消化 UI 逻辑。业务只需处理数据并响应数据给 UI,它不需要知道 UI 会怎样使用数据,更无权干预。

综上,无论是 UI 还是业务,都不应过度暴露内部逻辑 API 而受控于人,它们应只暴露请求 API,来响应外部的请求。过程逻辑应只在自己内部独立消化

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
public class XXXListBusinessProxy extends BaseBusiness<XXXBus> implements IXXXListFragmentRequest {

@Override
public void multiAddOrRemove(final XXXListDTO dto) {
handleRequest((e) -> {
...
if (TextUtils.isEmpty(existBsms)) {
sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE, false));
} else {
wyhcJgDBManager.insertAllTaskOfMine(existBsms, layersConfig);
sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE, true));
}
return null;
});
}

@Override
public void refreshPatternOfXXXList(final XXXListDTO dto) {
handleRequest((e) -> {
...
count.setMyXXXCount(wyhcJgDBManager.getMyXXXPatternCount());
return new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_COUNT, count);
});
}

@Override
public void changeXXXPatternOfMine(final XXXListDTO dto) {
handleRequest((e) -> {
if (toMine) {
...
} else {
...
sendMessage(e, new Result(XXXDataResultCode.XXX_LIST_FRAGMENT_GET_ALL_PATTERN_OF_MINE, count));
}
return null;
});
}
}



public class XXXListFragment extends BaseFragment implements IResponse {

XXXBus.XXX().queryList(mDto);

XXXBus.XXX().multiAddOrRemove(mDto);

XXXBus.XXX().queryPattern(mDto);

...

@Override
public void onResult(Result testResult) {
String code = (String) testResult.getResultCode();
switch (code) {
case XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_LIST:
updateRecyclerView((List<Wyhcrw>) testResult.getResultObject());
if (isNeedUpdateCount()) {
...
} else {
finishLoading();
}
break;
case XXXDataResultCode.XXX_LIST_FRAGMENT_MULTI_ADD_OR_REMOVE:
if ((boolean) testResult.getResultObject()) {
loadData(true, true);
} else {
ToastUtils.showShort(getContext(), "操作失败");
}
dismissProgressDialog();
break;
case XXXDataResultCode.XXX_LIST_FRAGMENT_REFRESH_PATTERN:
...
break;
default:
}
}
}

解耦有什么好处?

解耦的好处,福特最有话语权。

100 多年前,福特发明了世界上第一条流水线,让工人职责边界明确,从而得以分工和专注各自领域。

原先装配一辆车需 700 小时,通过流水线分工后,平均一辆 12.5 小时,这使得生产效率提升了近 60 倍!

软件工程同理。

由于 UI 和业务职责边界明确,且相互通过接口通信,使得 UI 和业务的编写者能够真正的分工

写 UI 的人,不会被业务的编写打断,他可以一气呵成的写自己的 UI。写业务的人,同样不会被打断,他可以专注于业务逻辑、数据结构和算法的优化。

写 UI 和写业务的人,都可以自己实现接口,去独立的完成单元测试,完全不必依赖和等候对方的实现。

最后,在职责边界明确的情况下,UI 就算写 100 个 UI 逻辑,那也是 UI,业务就算写 100 个业务,那也是业务,纯种,所以不会杂乱,何况我们还可以借助“接口隔离原则”继续往下分工!

总结

综上,本文介绍了两个重构思路:
1.顺应开闭原则,对定制化功能进行抽象。
2.顺应单一职责、最小知识、依赖倒置原则,让职责边界明确,防止代码耦合。

看完这篇文章,如你觉得有所收获和启发,请不吝点赞,你的点赞就是对我最大的支持!

本次项目重构用到的,符合设计模式原则的 viabus 架构,已在 GitHub 开源:
GitHub:KunMinX/android-viabus-architecture

更多文章

Android:四大架构的优缺点,你真的了解吗?
VIABUS - 年轻人的第一款架构
wiki - 1分钟掌握 ViaBus 架构的使用https://www.jianshu.com/p/6545767d3e54)

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×