写点什么

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

  • 2019-09-25
  • 本文字数:7591 字

    阅读完需:约 25 分钟

如何把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  }100101  /**102   * 执行在{@link #bindData(Object)}之后,{@link #onBindData(Object)}之前,{@link #isValid(Object)==true}且view没有初始化时才会回调103   */104  protected abstract void init(View view);105106  /**107   * 只用于子类实现,外界调用{@link #bindData(Object)}。108   * 该方法在{@link #isValid(Object) == true }时才回调109   *110   * @param data data111   */112  protected abstract void onBindData(T data);113114  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  }121122  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  }131132  /**133   * find target by tag,null if not exist134   *135   * @param tag tag136   */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  }147148  public T getData() {149    return mData;150  }151152  protected Context getContext() {153    return mContext;154  }155156  public LayoutInflater getInflate() {157    return mInflate;158  }159160  protected View getView() {161    return mView;162  }163164  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  }1112  @Override13  protected void init(View view) {1415  }1617  @Override18  protected void onBindData(T data) {1920  }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  } 910  interface View extends BaseView {11    void onGetHouseResult(@NonNull NewHouseResBlockDetailBean data);1213    void onUpdateStateLayout(boolean isSuccess, boolean netError);14  }1516  interface IFollowView extends IBaseView {17    /**18     * @param followed 是否已关注19     * @param error null表示成功,false表示失败20     */21    void updateFollowState(boolean followed, String error);22  }2324  interface IShareView extends IBaseView {25    void onShare(ShareDialog.ShareToThirdAppBean shareBean, ShareDialog.ShareToSmsBean smsBean);2627    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  }1011  public void attachShareView(HouseDetailContract.IShareView iShareView) {12    this.iShareView = iShareView;13  }1415  @Override16  public void getHouseResponse(String projectId) {17    enqueue(ApiClient.create(HouseApi.class).getHousesDetailResult(projectId),18      new SimpleCallback<Result<NewHouseResBlockDetailBean>>() {1920        @Override21        protected void onNetworkError(HttpCall<Result<NewHouseResBlockDetailBean>> call, Throwable t) {22          mView.onUpdateStateLayout(false, true);23        }2425        @Override26        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  }3738  @Override39  public void follow(String projectId, final boolean follow) {40    enqueue(ApiClient.create(HouseApi.class).followResblock(projectId, follow ? 1 : 0),41      new SimpleCallback<Result>() {42        @Override43        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); // 是否需要loading52  }5354  @Override55  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  }1011  @Override12  protected View onCreateView() {13    return inflate(R.layout.lib_newhouse_detail_encourages_policy);14  }1516  @Override17  protected void init(View view) {18    view.setOnClickListener(new View.OnClickListener() {19      @Override20      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  }2829  @Override30  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    } 910    // 绑定数据,更新UI11    public void bindData(NewHouseResBlockDetailBean bean) {12      ...13    }1415    @OnClick(R2.id.ll_attention)16    void onFollow() { // 关注/取消关注17        present.follow(bean.projectId, bean.isFollow == 0);18    }1920    ...2122}
复制代码


可以看到这里的 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


2019-09-25 08:001690

评论

发布
暂无评论
发现更多内容

集成学习中的随机森林

华为云开发者联盟

机器学习 决策树 随机森林 集成学习 Bagging

实测Tengine开源的Dubbo功能

捉虫大师

dubbo 网关 tengine

《复仇者联盟》AI换脸平台

不脱发的程序猿

人工智能 开源 AI 复仇者联盟

从源码中来,到业务中去,React性能优化终极指南

有道技术团队

大前端 React 有道精品课

合作伙伴眼中的HarmonyOS 专访方太智能厨电专家俞贵涛

科技汇

并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

MetaThoughts

Java 多线程 并发 并发王者课

聊聊微服务治理的落地问题 | Geek大咖说第二期

百度Geek说

微服务 自动化

MySQL 数据库救火:磁盘爆满了,怎么办?

华为云开发者联盟

数据库 磁盘 MySQL 数据库 日志文件 磁盘爆满

BI系统里的数据赋能与业务决策

薄荷点点

数据产品经理 决策 BI 数据驱动 风险识别

GitHub开源14.5万行阿波罗11号源代码

不脱发的程序猿

GitHub 开源 阿波罗11号

2021年爆火的低代码开发技术,对企业而言有什么好处?

优秀

低代码

要想成为牛人,推荐学习哪种编程语言?

实力程序员

【Flutter 专题】124 日常问题小结 (三) 自定义 Dialog 二三事

阿策小和尚

5月日更 Flutter 小菜 0 基础学习 Flutter Android 小菜鸟

☕️【Java 技术之旅】从底层分析LockSupport原理机制

码界西柚

Java JVM lock锁 5月日更 LockSupport

集群镜像:实现高效的分布式应用交付

阿里巴巴云原生

数据库 容器 开发者 云原生 存储

阿里云联合中国信通院发布《云计算开放应用架构》标准,加速云原生应用规模化落地进程

阿里巴巴云原生

容器 开发者 运维 云原生 k8s

Dubbo 序列化

青年IT男

dubbo

并发王者课-青铜6:借花献佛-如何格式化Java内存工具JOL输出

MetaThoughts

Java 多线程 并发 并发王者课

VSCode 无鼠标操作快捷键对比Atom

追风的少年

Django 之视图篇

若尘

django 视图 Python编程 5月日更

教你用User Story设计BI驾驶舱

薄荷点点

数据产品经理 用户故事地图 产品需求

谋而后动:解读数仓计划生成中行数估算和路径生成的奥秘

华为云开发者联盟

计划 数仓 GaussDB(DWS) 查询语句 估算

2021百度之星报名开启 特设“小星星”奖项鼓励少年AI人才

百度大脑

AI 百度之星 少年

Qemu KVM Guest增强简述

焱融科技

云计算 虚拟机 高性能 存储 qemu

索信达控股:金融机构如何打造最适合自己的个性化推荐系统?

索信达控股

大数据 金融科技 金融 个性化推荐 营销数字化

OCR性能优化:从神经网络到橡皮泥

华为云开发者联盟

神经网络 机器学习 OCR 橡皮泥 CNN网络

最佳入门系列 | 何为服务网关?

架构精进之路

微服务 5月日更

另一种总结的方式

Nydia

学习

哈工大与华为终端有限公司签署首个HarmonyOS高校协同育人合作协议

科技汇

网格策略交易软件,量化马丁倍投交易机器人

为什么你的Docker容器刚启动就停了?

运维研习社

Docker Linux 5月日更

如何把2000+行代码的详情页重构到不足200行_文化 & 方法_公台_InfoQ精选文章