写点什么

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

2019 年 9 月 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 年 9 月 25 日 08:00625

评论

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

第二周作业

远方

第三周作业三:优化 Cache 类的设计

远方

Java参数传递分析

游侠最光阴

Java

学习总结—第二周

于江水

架构是训练营

江帅帅:精通 Spring Boot 系列 01

古月木易

Spring Boot

第二周 软件设计原则

WW

ARTS - Week 3

Khirye

ARTS 打卡计划 arts

程序员的晚餐 | 6 月 15 日 红烧带鱼和清蒸多宝鱼

清远

美食

week2 学习总结

Geek_2e7dd7

架构师训练营第二周课后作业一

不谈

极客大学架构师训练营

做产品少走弯路:你必须掌握的知识

我是IT民工

产品 互联网 方法论 思维方式 知识体系

第二周作业二:描述熟悉的框架,是如何实现依赖倒置原则

远方

架构师训练营 第二周作业

大丁💸💵💴💶🚀🐟

用 Explain 命令分析 MySQL 的 SQL 执行

程序员历小冰

MySQL explian

架构师训练营 第二周 作业

一雄

极客大学架构师训练营 作业 第二周

江帅帅:精通 Spring Boot 系列 02

奈学教育

Spring Boot

架构师训练营第二周课后作业二

不谈

极客大学架构师训练营

游戏夜读 | 什么是全力以赴?

game1night

架构师训练营 第二周 学习总结

一雄

学习 极客大学架构师训练营 第二周

架构师训练营 第二周 作业

Poplar

第02周 开发编程框架 学习总结

Jaye

JVM的未来——GraalVM集成入门

孤岛旭日

Java 云原生 JVM GraalVM

第二周作业

andy

week2 作业

Geek_2e7dd7

架构师训练营 - 作业 - 第二周

心在飞

极客大学架构师训练营

Spring Aware 你不能不知道的事

CoderLi

Java spring 程序员 源码分析 后端

Class-only Protocols - class or AnyObject

SwiftMic

swift AnyObject

Week2命题作业

星河寒水

江帅帅:精通 Spring Boot 系列 02

古月木易

Sprint Boot

命题作业—第二周

于江水

极客大学架构师训练营

服务治理之轻量级熔断框架:Resilience4j

CoderJ

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