如何把 2000+ 行代码的详情页重构到不足 200 行

阅读数:363 2019 年 9 月 25 日 08:00

如何把2000+行代码的详情页重构到不足200行

最近在做重构,将一个 2000+ 行代码的详情页重构到不足 200 行。200 行的代码实现 2000+ 行代码的的业务逻辑?!当然不是了。我只是把原本耦合在一个页面的业务逻辑合理有效的拆分到多个模块中了。

1 背景

传统的 MVC 模式,业务代码都堆积在 Activity 中。随着业务需求的复杂,我们的页面文件代码量越来越多,类似房源详情这种可以达到 2K+ 行。

例如 Link 的新房详情页面,2182 行(单 onClick 回调方法占据了 265 行)。这样的代码存在着诸多问题,耦合性高,可读性差,维护成本非常大。本次采用新的架构方法(MVP+ 模块化),可以有效的解决以上问题。MVP 模式解耦合。模块化职责分明,大大减少单文件代码量,有效提升可读性,降低维护成本。

2 架构方案

基于 MVP 的设计模式,组合模块化(Part&ViewPart)。

3 方案概述

3.1 MVP

MVP 模式这里不多说,与传统的 MVP 模式不同的是,我们允许一个页面可以存在多个 P,多个 V,一个 P 可以对应多个 V。这样是为了我们每个模块可以完成自己的职责。P 层由 Activity 统一管理。当然如果一个 PV 独立于页面(且有复用性),则可以独立出这个 PV。

3.2 模块化

模块化:简单的讲就是讲业务拆分为多个模块,每个模块尽可能独立的负责自己的逻辑处理与 UI 展示。这样以来就实现了职责分明,并且大大减少了主 Activity 或 Fragment 的代码量,而且维护起来也比较方便。

3.2.1 模块化的方式

我们使用 Part&ViewPart 来实现模块化。

3.2.2 Part

Part 是我们一个页面的大容器中的每个模块,我们称之为 Part. 这里的大容器就是我们页面的流式容器,Part 要求对应的大容器必须是 LinearLayout。

我们提供一个 BasePart 抽象类,所有的 Part 都继承该类。我们先看一下 BasePart 的代码:

复制代码
1/**
2 * 模块化基类 <br/>
3 * 必须满足以下条件:<br/>
4 * 1. 父容器必须是 LinearLayout<br/>
5 * 2. 父容器的所有 view 都必须是通过 Part 添加的 <br/>
6 * 3. 必须通过 @#query 方法创建 part<br/>
7 * 4.View 添加位置必须固定且已知,暂不支持动态权重 <br/>
8 */
9public abstract class BasePart<T> {
10 private Context mContext;
11 private LayoutInflater mInflate;
12 private View mView;
13 private ViewGroup mParent;
14 private T mData;
15 private String tag;
16
17 public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz) {
18 return query(parent, clazz, null);
19 }
20
21 public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz, String tag) {
22 BasePart target;
23 if (TextUtils.isEmpty(tag)) {
24 tag = clazz.getSimpleName();
25 }
26 // 先根据 tag 从 parent 中取,如果没找到就创建。
27 // 这里不 check 数据,调用 bindData 方法后再处理数据,回调 onBindData
28 target = findPartByTag(parent, tag);
29 if (target == null) {
30 target = createPart(clazz);
31 target.mParent = parent;
32 target.mContext = parent.getContext();
33 target.mInflate = LayoutInflater.from(target.mContext);
34 target.tag = tag;
35 }
36 return (D) target;
37 }
38
39 public void bindData(T data) {
40 mData = data;
41
42 LinkedHashMap<String, Boolean> mStateMap = (LinkedHashMap) mParent.getTag(R.id.part_state);
43 if (mStateMap == null) {
44 mStateMap = new LinkedHashMap<>();
45 mParent.setTag(R.id.part_state, mStateMap);
46 }
47
48 if (isValid(data)) {
49 if (mView == null) {
50 mView = onCreateView();
51 ButterKnife.bind(this, mView);
52 mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
53 @Override
54 public void onViewAttachedToWindow(View v) {
55
56 }
57
58 @Override
59 public void onViewDetachedFromWindow(View v) {
60 onDestroyView();
61 }
62 });
63 // fixme 只有数据有效的 part 才会存储到 view 中
64 mView.setTag(R.id.part_cache, this);
65 // 把这个对象和 view 绑定
66 init(mView);
67 // 如果当前存储的数据中不包含这个 tag,那么就直接 add
68 if (!mStateMap.containsKey(tag)) {
69 mParent.addView(mView);
70 } else {
71 mParent.addView(mView, findAddPosition(mStateMap, tag));
72 }
73 mStateMap.put(tag, true);
74 }
75 onBindData(data);
76 } else {
77 if (mView != null) {
78 mParent.removeView(mView);
79 }
80 mStateMap.put(tag, false);
81 }
82 }
83
84 protected final View inflate(@LayoutRes int resource) {
85 return mInflate.inflate(resource, mParent, false);
86 }
87
88 /**
89 * 数据是否有效
90 *
91 * @param data data
92 * @return false will remove relative view
93 */
94 protected abstract boolean isValid(T data);
95
96 protected abstract View onCreateView();
97
98 protected void onDestroyView() {
99 }
100
101 /**
102 * 执行在{@link #bindData(Object)}之后,{@link #onBindData(Object)}之前,{@link #isValid(Object)==true}且 view 没有初始化时才会回调
103 */
104 protected abstract void init(View view);
105
106 /**
107 * 只用于子类实现,外界调用{@link #bindData(Object)}。
108 * 该方法在{@link #isValid(Object) == true }时才回调
109 *
110 * @param data data
111 */
112 protected abstract void onBindData(T data);
113
114 private static <D extends BasePart> D createPart(Class<D> clazz) {
115 try {
116 return clazz.getConstructor().newInstance();
117 } catch (Exception e) {
118 throw new RuntimeException(e);
119 }
120 }
121
122 private int findAddPosition(LinkedHashMap<String, Boolean> hashMap, String tag) {
123 // 查找指定 tag 之前的所有被添加的 view 的个数,作为 add 时候的 index,必须保证该容器的所有 view 都是通过 part 添加的
124 int count = 0;
125 for (Map.Entry<String, Boolean> entry : hashMap.entrySet()) {
126 if (entry.getKey().equals(tag)) return count;
127 if (entry.getValue()) count++;
128 }
129 return count;
130 }
131
132 /**
133 * find target by tag,null if not exist
134 *
135 * @param tag tag
136 */
137 private static BasePart findPartByTag(ViewGroup parent, String tag) {
138 for (int i = 0; i < parent.getChildCount(); i++) {
139 View view = parent.getChildAt(i);
140 BasePart part = (BasePart) view.getTag(R.id.part_cache);
141 if (part.tag.equals(tag)) {
142 return part;
143 }
144 }
145 return null;
146 }
147
148 public T getData() {
149 return mData;
150 }
151
152 protected Context getContext() {
153 return mContext;
154 }
155
156 public LayoutInflater getInflate() {
157 return mInflate;
158 }
159
160 protected View getView() {
161 return mView;
162 }
163
164 protected ViewGroup getParent() {
165 return mParent;
166 }
167}

简单描述下,BasePart 所做的事情:根据子类判断的数据是否有效,当数据有效时才会将子模块 view 添加到父容器中。这样无效的数据模块也就不会被添加进来,避免不必要的渲染浪费。添加进来的 view 会和对应的 part 做一个映射,并将 part 缓存下来,支持单独刷新某个 Part。

下面来看下 Part 的使用方式。

首先我们要实现一个 Part,BasePart 是个抽象类,必须实现 4 个抽象方法。

复制代码
1public class XxxPart extends BasePart<T>{
2 @Override
3 protected boolean isValid(T data) {
4 return false;
5 }
6
7 @Override
8 protected View onCreateView() {
9 return null;
10 }
11
12 @Override
13 protected void init(View view) {
14
15 }
16
17 @Override
18 protected void onBindData(T data) {
19
20 }
21}
  • isValid 用于判断数据是否有效,数据有效,我们的模块才会被加载,避免不必要的渲染
  • onCreateView 用于创建 View
  • init 用于初始化
  • onBindData 用于绑定数据

除了上面四个必须实现的方法,BasePart 还提供一个非抽象的方法,==onDestroyView()==,用于处理销毁逻辑,如 Handler,请求等。

接下来看一下,Part 是怎么加载、调用的。Part 的使用很简单,只需要调用 query 方法,并 bindData 就够了。query 方法返回 Part,因此支持模块间的相互调用及跨模块更新。

复制代码
1public static <D extends BasePart> D query(LinearLayout parent, Class<D> clazz, String tag){...}

简单分析一下。query 需要 3 个参数。

  • parent 即该模块所在的父容器,也就是我们前面说的页面大容器。
  • clazz 模块 Part 的 class 对象(是的,这里用的反射构建的 Part 的实例)
  • tag 每个 Part 都会有一个 tag 与之对应,为了我们缓存 part。如果没有设置,会默认取 class.simpleName

3.2.3 ViewPart

前面有说到我们这里的模块化分为 Part 和 ViewPart。这里的 ViewPart 其实就是一个 View。在非大容器中的模块,我们可以使用 ViewPart 的方式将其模块化。我们模块化的原则是尽量将各个模块的业务交由各个模块自己完成,职责分明,逻辑清晰。

简单介绍完这些概念之后,下面来具体看一下重构的过程。

4 重构过程

4.1 梳理逻辑模块

新房房源详情页大大小小加一起,一共 14 个业务逻辑。其中除引导图外,其他我们均采用模块化的方式处理。分享逻辑和底部 Bar 采用 ViewPart 的方式,其他采用 Part 的方式。HouseDetailActivity 采用 MVP 的模式。

  • 分享引导图
  • 分享逻辑
  • 楼盘信息模块
  • 一房一价模块
  • 其他信息模块
  • 竞对盘推荐模块
  • 激励政策 模块
  • 动态模块
  • 特价房模块
  • 客户规则模块
  • 佣金 * 规则模块
  • 楼盘销售特点模块
  • 户型模块
  • 底部 bar

4.2 主页面 MVP 架构

首先看一下,详情页整体的 MVP 契约类。

复制代码
1public interface HouseDetailContract {
2 interface Present extends BasePresenter<View> {
3 void getHouseResponse(String projectId);
4
5 void getShareResponse(String projectId);
6
7 void follow(String projectId, boolean follow);
8 }
9
10 interface View extends BaseView {
11 void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data);
12
13 void onUpdateStateLayout(boolean isSuccess, boolean netError);
14 }
15
16 interface IFollowView extends IBaseView {
17 /**
18 * @param followed 是否已关注
19 * @param error null 表示成功,false 表示失败
20 */
21 void updateFollowState(boolean followed, String error);
22 }
23
24 interface IShareView extends IBaseView {
25 void onShare(ShareDialog.ShareToThirdAppBean shareBean, ShareDialog.ShareToSmsBean smsBean);
26
27 void onGetShareResultFail(String error);
28 }
29}

可以看到,这里一个 P,有多个 V。且逻辑中还有两个比较独立且被复用的 PV,抽离出来了,这里不再列举。简单看下主 Present 的实现。

复制代码
1public class HouseDetailPresent extends HttpPresenter<HouseDetailContract.View>
2 implements HouseDetailContract.Present {
3
4 private HouseDetailContract.IFollowView iFollowView;
5 private HouseDetailContract.IShareView iShareView;
6
7 public void attachFollowView(HouseDetailContract.IFollowView iFollowView) {
8 this.iFollowView = iFollowView;
9 }
10
11 public void attachShareView(HouseDetailContract.IShareView iShareView) {
12 this.iShareView = iShareView;
13 }
14
15 @Override
16 public void getHouseResponse(String projectId) {
17 enqueue(ApiClient.create(HouseApi.class).getHousesDetailResult(projectId),
18 new SimpleCallback<Result<NewHouseResBlockDetailBean>>() {
19
20 @Override
21 protected void onNetworkError(HttpCall<Result<NewHouseResBlockDetailBean>> call, Throwable t) {
22 mView.onUpdateStateLayout(false, true);
23 }
24
25 @Override
26 public void onResponse(HttpCall<Result<NewHouseResBlockDetailBean>> call,
27 Result<NewHouseResBlockDetailBean> entity) {
28 if (hasData()) {
29 mView.onGetHouseResult(entity.data);
30 mView.onUpdateStateLayout(true, false);
31 } else {
32 mView.onUpdateStateLayout(false, false);
33 }
34 }
35 });
36 }
37
38 @Override
39 public void follow(String projectId, final boolean follow) {
40 enqueue(ApiClient.create(HouseApi.class).followResblock(projectId, follow ? 1 : 0),
41 new SimpleCallback<Result>() {
42 @Override
43 public void onResponse(HttpCall<Result> call, Result entity) {
44 if (isSuccess()) {
45 iFollowView.updateFollowState(follow, null);
46 } else {
47 iFollowView.updateFollowState(follow,
48 Result.getErrorMsg(entity, follow ? " 关注失败 " : " 取消关注失败 "));
49 }
50 }
51 }, true); // 是否需要 loading
52 }
53
54 @Override
55 public void getShareResponse(String projectId) {
56 ...
57 }
58}

主 Present 会绑定其他两个模块的 V,并完成对应的 P 层逻辑和 V 的回调。

再来看下我们的详情页,这里重点看下,获取到数据之后加载 part 的处理逻辑。

复制代码
1@Override
2 public void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data) {
3 this.bean = data;
4 setTitle(data.name);
5 bottomPart.bindData(data);
6 sharePart.bindData(data.projectName);
7 initDMShare();
8 BasePart.query(container, HouseDetailInfoPart.class).bindData(data);
9 BasePart.query(container, OnePricePart.class).bindData(data);
10 BasePart.query(container, HouseDetailOtherInfoPart.class).bindData(data);
11 BasePart.query(container, RecommendPart.class).bindData(data);
12 BasePart.query(container, IncentivePolicyPart.class).bindData(data.incentivePolicy);
13 BasePart.query(container, DynamicPart.class).bindData(data.newDynamic);
14 BasePart.query(container, DiscountHousePart.class).bindData(data);
15 BasePart.query(container, CustomerRulePart.class).bindData(data.customerRule);
16 BasePart.query(container, CommissionRulePart.class).bindData(data.commissionRule);
17 BasePart.query(container, SaleFeaturePart.class).bindData(data.saleFeature);
18 BasePart.query(container, FramePart.class).bindData(data);
19 }

所有根据返回数据操作 UI 及逻辑都在这个方法里。其中 bottomPart 和 sharePart 就是两个普通的 View。

下面看一个简单的 Part。

复制代码
1public class IncentivePolicyPart extends BasePart<NewHouseResBlockDetailBean.IncentivePolicy> {
2
3 @BindView(R2.id.tv_encourage_policy_detail)
4 CommonTextView tvEncouragePolicyDetail;
5
6 @Override
7 protected boolean isValid(NewHouseResBlockDetailBean.IncentivePolicy data) {
8 return data != null && !TextUtils.isEmpty(data.excitationPolicy);
9 }
10
11 @Override
12 protected View onCreateView() {
13 return inflate(R.layout.lib_newhouse_detail_encourages_policy);
14 }
15
16 @Override
17 protected void init(View view) {
18 view.setOnClickListener(new View.OnClickListener() {
19 @Override
20 public void onClick(View v) {
21 if (!TextUtils.isEmpty(getData().excitationPolicyUrl)) {
22 CommonWebActivity.startActivity(getContext(), getData().excitationPolicyUrl,
23 getContext().getString(R.string.newhouse_jili_zhengce));
24 }
25 }
26 });
27 }
28
29 @Override
30 protected void onBindData(NewHouseResBlockDetailBean.IncentivePolicy bean) {
31 tvEncouragePolicyDetail.setText(bean.excitationPolicy);
32 }
33}

View 创建和初始化,数据源的有效性判断和数据绑定,层次分明,方便维护。

ViewPart 的代码稍多一些,这里简单取一部分代码。

复制代码
1public class HouseDetailBottomPart extends LinearLayout implements HouseDetailContract.IFollowView {
2 ... // ButterKnife 初始化 view
3
4 // 绑定 present 和 V
5 public void setPresent(HouseDetailPresent present) {
6 this.present = present;
7 present.attachFollowView(this);
8 }
9
10 // 绑定数据,更新 UI
11 public void bindData(NewHouseResBlockDetailBean bean) {
12 ...
13 }
14
15 @OnClick(R2.id.ll_attention)
16 void onFollow() { // 关注 / 取消关注
17 present.follow(bean.projectId, bean.isFollow == 0);
18 }
19
20 ...
21
22}

可以看到这里的 PartView 也用了 MVP,自身便是个 V。

到这里,我们已经大致看完了,Part 模式相关的全部内容。下面来总结下 Part 的优点和存在的不足。

5 优点

  • 大量减少主控制器(Activity or Fragment) 代码量
  • 层次分明,解耦。各业务模块的业务由模块自身处理
  • 无效的模块将不会被加载,避免不必要的渲染
  • 支持模块局部刷新。某模块数据变化时,直接刷新该模块即可,不需要重建 view,不影响其他模块
  • 支持模块间的相互调用

6 不足

Part 的方式并不适用所有的页面,part 主要解决的是线性容器下多模块的场景

  • 父容器必须是 LinearLayout 且父容器的所有子 view 都必须是通过 Part 添加的
  • 必须通过 query 方法创建 part
  • View 添加位置必须固定且已知,暂不支持动态权重

作者介绍:
公台(企业代号名),目前负责贝壳找房新房 B 端 Android。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/LLt8u5WVLxLrg_acMXLvEA

评论

发布