驴妈妈客户端频道页模块化设计思路

Hello ,iOSTips的读者朋友们大家好,我是来自驴妈妈的【傅说君】,喜欢一本正经的傅说八道。由于老峰已经放假出去浪了,今天的文章由我来分享,Trust me,傅说君分享的都是干货,嗯比老峰的干。

今天分享一下驴妈妈客户端频道页的模块化设计思路,国庆后会分享有关App架构系列的文章2~3篇,主要会覆盖App架构中的界面搭建、架构分层、以及模块组件化等等 ,内容着重于围绕职责讨论代码的角色划分以及设计模式对开发流程的影响(这次会附demo),我会尽量不同于过往的精品文章,深坑已经挖好,具体怎么填就看你们的热情度了,国庆后咱们周一见。

本文主要分享实现频道页模块化的大体思路, 并不涉及具体代码实现.由于各位看官老爷的"赞"真香,我最近又更新并追加了部分内容。

640?wx_fmt=gif

零、目录

全文字数: 4,156 | 预计阅读: 16分钟

  • 一、需求分析

  • 二、模块定义

  • 三、模块化设计原则

    • 3.1 面向接口

    • 3.2 数据驱动

    • 3.3 模块隔离

  • 四、模块化框架设计

    • 4.2.1 组件协议

    • 4.2.2 模块组件数据模型

    • 4.1.1 数据源协议

    • 4.1.2 模块组件管理

    • 4.1.3 数据流向

    • 4.1 数据源

    • 4.2 模块组件

    • 4.3 频道代理对象协议

    • 4.4 对象通信

    • 4.5 交互图

  • 五、小结

一、需求分析

为了满足运营同学动态配置频道页的内容排版, 以及产品同学一次开发, 各频道复用的需求, 要开发一个框架来满足以下两点:

  • 内容灵活排版: 某个频道页展示的内容及其顺序, 甚至说一个新的频道页, 皆可由运营同学在线上环境直接配置

  • 模块全局复用: 一个承载内容的 模块 开发完后, 可在全部频道页配置使用

二、模块定义

上文提及的 内容 , 就是我们各频道页看到的 模块. 不同的 模块 具有其独特的产品功能与运营目的. 

以驴妈妈首页频道为例, 如下图:

640?wx_fmt=png

每个框所圈区域为一个独立模块. 比如:

  • banner模块(产品推荐、活动推广、广告投放等)

  • 频道入口、主题列表模块(用户分流导向)

  • 旅行头条模块(热门游记推荐)

  • ...

此外, 每个模块可以包含单个或多个不同的模块组件:

640?wx_fmt=png

三、模块化设计原则

除了考虑SOLID(六大原则)外, 框架设计还会围绕以下三点.

3.1 面向接口

通过定义 接口(即协议) 抽象和规范框架所关心的类或事. 框架与模块间低耦合.

举个例子, 对于框架来说, 它并不关心配置数据是什么结构或如何获取, 它仅关心的是有多少个模块、每个模块在容器中所占大小以及位置等数据.

可为此定义一个数据源协议, 来规范充当框架数据源对象所必须遵循的行为. 至于数据源对象的具体类型是什么不重要, 只要遵循协议即可充当框架中的某个角色.

当然, 面向接口与面向对象并不冲突, 反而是相辅相成, 此处不展开讨论.

3.2 数据驱动

数据决定并驱动内容的展示与响应.

  • 数据决定展示内容, 即数据与内容一一对应:

    框架根据数据源提供的相关数据, 决定每个模块该创建的组件类型, 模块组件的展示大小及布局位置等.

  • 数据驱动内容变化. 关注点为数据, 而非事件. 

    举个例子, 对于框架中模块发生的任意事件, 其结果也就两种:

    640?wx_fmt=png

    也就说框架不会去管具体发生什么事件, 只"盯着"它所关心的数据有没有变

    另外, 事件驱动中一个事件往往对应一个响应操作, 是1对1的关系. 而数据驱动可以是1对N的关系, 可能是多个事件修改同个数据.

  1. 事件发生后, 数据有变化

  2. 事件发生后, 数据无变化

3.3 模块隔离

模块间相互隔离, 模块独立自治, 其相关事务自行处理.

模块可单独开发, 注册到框架中. 模块内可自行使用MVX、VIPER等结构型设计模式(Structual Design Pattern).

此外, 模块联合开发中, 框架与模块隔离, 不产生依赖倒置. 模块依赖框架提供的功能进行开发, 而框架不能依赖模块内容, 框架仅知道我们抽象出来的接口, 接口细节由模块去实现.

通俗来说就是框架的功能开发与模块的开发是两条平行线, 模块开发过程中, 不需要向框架中引入模块的头文件. 

这一块的具体解耦会下一章节的中的 接口定义以及 模块管理 有所体现.

四、模块化框架设计

以iOS平台举例, 阐述对整个框架的具体设计. 抛开Android和iOS平台系统编码的风格习惯和具体实现上存在的不同, 整体思想大同小异.

4.1 数据源

一个频道页由若干个模块组成, 一个模块包含1个或多个不同的组件. 框架根据数据源提供的信息, 创建和安置模块组件. 

4.1.1 数据源协议

模块数据源协议: 主要向框架提供某个模块包含的组件信息、相关的布局信息、以及组件填充数据的内容等等

typedef NSObject<LVTSectionDataSource> LVTSectionData;

频道页数据源协议: 主要向框架提供整个频道拥有的模块总数, 以及各模块的局部数据源

@protocol LVTPageDataSource <NSObject>

4.1.2 模块组件管理

对于模块内的任意组件, 都有对应一个标识ID. 我们通过一个配置文件来维护标识与组件的对应关系. 联合开发时, 每开发好一个新的组件, 就只用修改配置文件, 不动框架代码.

配置的JSON结构大致如下:

// 部分举例

{
    "header": {
        "header1": "LVTXXXHeader", // value为具体类名
        "header2": "LVTXXXHeader",
        ...
    },
    "cell": {
        "cell1": "LVTXXXCell",
        ...
    },
  ...
}

通过ID我们可获得一个具体的类名, 再使用反射获得类对象以供框架创建组件实例. 

在iOS上我们通过一个ClassMapper来专门维护对应关系, 如下图.

640?wx_fmt=png

上图类名仅为更好的表达Mapper的职责, 实际ClassMapper返回的类对象会使用泛型来进行解耦, ClassMapper中也不要引入任何组件的头文件.

4.1.3 数据流向

从原始数据到呈现到屏幕上的每个模块组件, 数据流向如下图所示:

640?wx_fmt=png

上图各元素代表:
LVTPageDataSource 为遵循频道数据源协议的对象
LVTSectionData 为遵循模块数据源协议的对象
ClassMapper 为管理对应关系的对象
LVTCellXXX、LVTHeaderXXX为组件等

4.2 模块组件

组件是模块化框架中复用的基础元素. 

4.2.1 组件协议

模块组件分为可复用与不可复用两类, 分别对应以下协议:

复用组件协议: 提供组件用于复用队列的复用Id、用于布局的元素大小等

typedef Class<LVTReuseItemProtocol> LVTemplateClass;
typedef UICollectionViewCell<LVTReuseItemProtocol> LVTemplateCell;
typedef
根据Model计算复用组件的大小. 若高度固定, 则直接返回(容器宽度, 固定高度)即可

@param model id<LVTItemModelProtocol> 遵循该协议的模型对象
@param size 容器CollectionView的大小, 用于均分计算*/
+ (CGSize)itemSizeWithModel:(LVTItemModel *)model andContainerSize:(CGSize)size;
#pragma mark - 配置/**
根据传入Model配置组件内容. 会持有传入model. 子类实现需先调用super方法.

子类在该方法中进行数据填充, 以及通过事件中心进行 相关的事件注册*/
- (void)configItemWithModel:(LVTItemModel *)model;
/** 设置事件中心 */
- (void)setEventCenter:(id<LVTEventCenterProtocol>)center;
/** 当前频道模块组件缓存工具 */
- (void)setCacheUtil:(LVTCacheUtil *)util;

@optional

/** 不同于系统的prepareForReuse(在复用队列dequeue时才调用), 该方法会在入队时调用 */

- (void)itemPrepareForReuse;
// ---------- 以下方法仅需要BaseCell实现 ----------/** 设置组件在Section中的Index序号 */

- (void)setIndex:(NSUInteger)index;/** 设置分割线是否隐藏 */

- (void)setSeparatorHidden:(BOOL)hidden;/** 设置分割线水平边距 */

- (void)setSeparatorLineMargin:(LVTLineMargin)margin;
@end

不可复用的悬浮组件协议: 提供视图高度, 悬浮定位信息等

typedef Class<LVTFloatViewProtocol> LVTFloatViewClass;

数据填充等公共方法可抽象到另一个协议中, 再进行继承

4.2.2 模块组件数据模型

用于填充模块组件的数据模型类型不一, 框架也不与具体模型产生瓜葛. 通过协议规范数据模型得有的属性即可.

数据模型协议:

typedef NSObject<LVTItemModelProtocol> LVTItemModel;
@protocol LVTItemModelProtocol <NSObject>

/** Cell内容是否折叠 */
@property (nonatomic, assign) BOOL isFolded;

/** Cell内容完全展示时的大小 */
@property (nonatomic, assign) CGSize itemSize;

/** Cell内容折叠时的大小 */
@property (nonatomic, assign) CGSize foldedItemSize;

/** SectionHeader高度 */
@property (nonatomic, assign) float headerHeight;

/** 悬浮View高度 */
@property (nonatomic, assign) float floatViewHeight;

@end

我们在前边协议中看到的LVTItemModel即代表了遵循该协议的数据模型

4.3 频道代理对象协议

以上我们说的那些模块在框架中的位置都是可调整的. 对于频道中位置固定的内容, 比如导航栏, 容器页头, 页脚等元素, 会交给一个频道的 代理对象 来处理. 

640?wx_fmt=png

除了固定内容的管理, 还有一些与框架无关联的业务功能, 比如点位获取、站点切换等功能, 也会放到代理对象里边实现, 但不在协议里边体现.

具体代理协议如下:

@protocol LVTPageDelegate <NSObject>

#pragma mark - 服务类对象/** 通过事件中心注册相关事件, 以进行一些操作. 比如, 导航栏颜色变化、透明度变化等 */
@property (nonatomic, weak) LVTEventCenter *eventCenter;

/** 布局信息查询器 */
@property (nonatomic, weak) LVTLayoutQuery *layoutQuery;

#pragma mark - 频道表现控制/** 容器顶部间隔, 默认返回statusBar+导航栏高度 */
@property (nonatomic, readonly) CGFloat containerViewTopInset;

/** 是否隐藏TabBar. 默认为YES. */
@property (nonatomic, readonly) BOOL hidesBottomBarWhenPushed;

/** 首屏加载或者下拉刷新是否显示 小驴加载动画. */
@property (nonatomic, readonly) BOOL showsLoadingIndicator;

#pragma mark - 由代理负责创建的对象/** 子类重写getter方法返回nil则没有header. 基类实现默认会创建MJRefreshNormalHeader实例 */
@property (nonatomic, strong) MJRefreshHeader *header;

/** 子类重写getter方法返回nil则没有footer. 基类实现默认会创建MJRefreshAutoNormalFooter实例 */
@property (nonatomic, strong) MJRefreshFooter *footer;

#pragma mark - 托管对象/** 托管给代理的导航栏, 以便修改颜色变化、透明度变化等 */
@property(nonatomic, weak) UINavigationBar *navigationBar;

/** 托管给代理的便捷配置的导航栏item */
@property (nonatomic, weak) UINavigationItem *navigationItem;

/** 托管给代理的主View, 用于添加自定义悬浮View等. 对应在prepareForDispose进行移除. */
@property (nonatomic, weak) UIView *view;

#pragma mark - 自定义配置/** 在该方法进行 导航栏元素创建、Header、Footer创建以及事件注册等 */
- (void)setupPageUI;

/** 根据接口下发数据动态配置样式. 比如搜索框默认文字、默认站点等 */
- (void)configPageWithModel:(id)model;

/** 替换代理对象时, 销毁上一个代理对象, 可在该方法内移除由该代理添加的一些视图等操作 */
- (void)prepareForDispose;
@end

该代理的管理, 与模块管理一致, 共用配置文件, 代理类对象的获取也是通过ClassMapper.

另外, 严格意义上讲, 把这个delegate命名为strategy会更加合适, 它的使用体现是 策略模式. 不同的代理有着不同的实现, 某个频道运行时, 也可能会动态的切换代理对象. 

比如, 某次下拉刷新后, 下发的代理ID变了, 即对应的类对象变了, 就会创建新的 策略对象 来替换, 从而产生了不一样的UI或行为表现.

4.4 对象通信

模块之间, 模块与框架间存在相互通讯的需求. 比如在某些模块组件需要知道框架存在的生命周期事件, 以作出对应的操作. 

对象间的常见通讯方式有:

  1. 命令模式或Target-Action

  2. 代理模式或回调Callback

  3. 观察者模式

考虑到模块间通讯可以1对多, 而前面两种皆为1对1通讯, 所以我们选择基于ReactiveCocoa或RxJava库, 遵循观察者模式来实现一个囊括所有跨模块事件的共享对象, 以进行集中式管理. 以下称之为 事件中心.

具体来说, 就是把有通信需求模块的相关事件集, 以空方法的形式统统添加到事件中心的共享对象上暴露出来(方法实现为空, 但并非抽象类). 各模块则根据自己的需求, 选择性的订阅共享对象上的事件. 

640?wx_fmt=png

模块通讯方式则为直接调用共享事件中心上已添加好的事件方法, 如下:

640?wx_fmt=png

 4.2 模块组件一节中的两个协议里, 都可见定义了设置事件中心的方法以供框架赋值, 以供组件访问.

4.5 交互图

整个框架核心元素间的交互如下:

640?wx_fmt=png

上图没有包括具体的交互细节, 补上两张时序图:

  • 某频道页首屏展示的时序图(忽略本地缓存等各种情况):

    640?wx_fmt=png

    注: 模块的数据源(SectionData)会向ClassMapper获取具体的类对象, 具体可见数据源协议

  • 某个框架事件(比如切换界面、滑动等)通过事件中心传递给订阅者的时序图:

    640?wx_fmt=png

    事件传递为同步操作, 哪个线程调用哪个线程触发, 订阅者接收事件的顺序由订阅时的先后顺序决定

五、小结

以上便为驴妈妈频道页模块化的大致思路, 思路不复杂, 主要细节繁多, 就不一一展开. 

无论何种实现方案, 在灵活满足业务需求的前提下, 同时保证技术上的拓展性, 未来再不断"打怪升级", 都不失为一个较优解.

最后,小长假即将到来,傅说君预祝各位玩的开心,外出请注意安全,enjoy。

640?wx_fmt=png

更多骚操作,尽在iOSTips,关注公众号,第一时间get新姿势

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值