【AICon】AI 基础设施、LLM运维、大模型训练与推理,一场会议,全方位涵盖! >>> 了解详情
写点什么

如何把 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:001268

评论

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

大模型训练中的高性能计算与通信优化

百度开发者中心

深度学习 大模型 #人工智能

提速30%!HarmonyOS NEXT自动化测试开发效率提升

新消费日报

AI对抗中的AI:技术展望与应用研究

EquatorCoco

人工智能 AI

软件测试/测试开发丨接口自动化学习笔记——响应体断言

测试人

软件测试 接口测试

Adobe Camera Raw 16(RAW处理工具)官方版下载

影影绰绰一往直前

Adobe Camera Raw下载 Adobe Camera Raw中文 Adobe Camera Raw破解 RAW处理工具

调用API接口获取淘宝商品数据:详细指南与代码实践

Noah

八个提升编程体验的VS Code插件

这我可不懂

vs-code

Adobe Photoshop Lightroom中文特别版下载(Lr2024)

影影绰绰一往直前

lr2024 Adobe Lightroom下载 Adobe Lightroom特别版 Adobe Lightroom中文版

数据赋能业务,神州数码HR数字画像荣获2023HRoot人力资源管理卓越实践奖

科技热闻

少林寺方丈释永信造访 Meta 总部;OpenAI 正在摧毁创业公司?丨 RTE 开发者日报 Vol.80

声网

国外服务器购买必备知识:如何避免常见陷阱?

一只扑棱蛾子

SAE 2.0,让容器化应用开发更简单

Serverless Devs

云计算 负载均衡 Serverless

Capture One 23 Pro专业版下载(图片编辑软件)

影影绰绰一往直前

Capture One Pro 23 Capture One 破解版 Capture One下载

HarmonyOS应用开发

不在线第一只蜗牛

华为 架构 系统 HarmonyOS

Adobe Substance 3D Painter(pt3D绘画软件)激活版下载

影影绰绰一往直前

Substance 3D Painter下载 Substance 3D Painter破解

Macos视频下载工具:Downie 4 支持M1

彩云

视频下载 downie 4

Linux中比cp好用10倍的rsync,你会用了吗

高端章鱼哥

Linux rsync

软件测试/测试开发丨接口自动化学习笔记——请求方法构造

测试人

软件测试 接口测试

加密货币交易软件开发:树立行业新标准

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

重庆上百位老师和学生,正在使用这个国产操作系统

OpenCloudOS

Linux 操作系统

杭州悦数出席 2023 云栖大会计算巢专场,分享云上最佳实践

最新动态

大模型训练,推动NLP发展的强大引擎

百度开发者中心

自然语言处理 大模型训练 #人工智能

新手初探Amazon Lightsail,几步构建一个Wordpress服务器

王强

VPS云主机 亚马逊云服务器

软件测试/测试开发丨接口测试学习笔记,TcpDump与WireShark

测试人

软件测试 接口测试

软件开发全套资料整理下载(投标支撑,立项,研发,测试,实施维护,安全监测,服务巡检,结项,验收支撑)

代码人,代码魂

Axure RP Pro8(原型设计工具)汉化特别版下载

影影绰绰一往直前

Axure RP Pro下载 Axure RP Pro破解版 Axure RP Pro中文版

第三方数据测评对比五大品牌HTTP代理!哪家代理最纯净稳定

Geek_bf375d

大模型时代,程序员的工作还是“写程序”?

SoFlu软件机器人

程序员 软件开发 AIGC java 技术提升

AutoCAD 2024 中文版 附 完整图文安装激活教程 支持M1

彩云

mac软件下载 AutoCAD 2024

DDD技术方案落地实践

EquatorCoco

技术 DDD 教程 教程分享

致敬记者节,合合信息扫描全能王助力新闻工作者构建“随身资料库”

合合技术团队

人工智能 合合信息 扫描全能王 记者节 新闻工作者

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