【设计总结】模块设计通用范式 -- 基础设施 -> 通用功能 -> 业务封装 三层结构

聊代码设计时通常聊什么? 高内聚松耦合接口隔离单一职责
这里不聊这些名词,几年的经验让我体会到,常说的代码设计思路,设计模式,是在面临实际问题后会自然而然的形成的一种解决方案,
但若没面对过,尝试用这些概念搭建代码,总有些像空中楼阁

15~16 年,随着做的项目增多,并且有自己要独立做 APP 经历后,发现代码中存在一些共性,比如

  • 有些部分承担最基础的工作,最好让他独立,不绑定状态。 因为用的地方太多了
  • 一个业务上的功能, 可能很多业务都有用到,但是细节又各不相同, 可以抽出来做独立类
  • Controller 管了最多的逻辑,逻辑之间可能还互相交叉
  • 代码一层,一层实现,这样越底层越通用

类似的规则,潜移默化的在工作中积累了很多条
直到 18 年,负责起整个团队的项目,需要做好 APP 的基础结构,并且让其他同事也这么做时。
潜意识的概念不管用了, 必须成文,理清楚适用性。这时才发现很多东西是一知半解,意识流。而补充资料的过程发现这就是曾觉得高高在上的的 做架构
(限于项目规模,属于小打小闹层面)

因为是个很 “概念化” 的话题, 我很难用精准的语言标准给每一层做定义, 暂时先靠例子。可以以后接触更多专业化名词或概念后,能加以改善


先看结论

优势:

  • 最外层调用业务封装,无需关心实现细节。 底层对上层隐藏实现,每层只关心同层级的事务
  • 在遵循同一套接口的情况下,随意替换下层(比如换一套数据库)
  • 新的业务,可方便的调用底层组装出来
  • 通用功能部分,可方便的移植到不同模块,不同 APP
  • 底层只实现”单一“功能的情况下, 可以很方便的延伸出不同的上层

概念:

  • 代码设计通常可分三层:
    最底层是基础的单一功能实现。 着重于功能实现。 适用于任何需要这个功能的场景,易于延伸,拼装。但通常直接使用繁琐
    中间层是对基础功能的延伸或组合。 着重于逻辑上功能的延伸和组合。 适用于具备相同流程,相同功能的场景,可移植到不同模块/APP
    再往上直接封装某个业务,是绑定状态,绑定 APP 环境的中间层。 仅适用当前的 APP

  • 称之为: 基础设施通用功能业务封装
  • 基础设施提供 单一操作 ,目的是能被任意组合成不同的复杂功能。 通用功能 不假设环境,不绑定状态 ,目的是能被不同的业务需求所使用。
  • 依赖方向是上层依赖下层:通常 在业务层是一对多依赖,通用功能是一对一依赖,基础设施依赖系统 API。
    从逻辑结构角度看,不允许底层依赖上层
  • 层与层的关系,可以形象的理解成”树“。 但调用上,最外层业务也可能调用最底层接口

ps0.“层”的概念,不是指只能有三个类分别代表这三层。 更多的是“角色“概念 ,看具体承担的职责,可以有 3,4,5,6 层
ps1.分层不解决依赖混乱问题, 比如业务封装, 需要自己在实现时,确保只包含单一业务。不要实现和依赖不属于自己职责的东西


基础设施

我们写代码时, 始终有一部分最底层的,做着真正功能实现(而非逻辑流程)部分,常见的比如系统提供的各种 API,基于系统实现的自定义功能接口
在 文件管理器 模块: 读文件,创建文件夹,判断文件是否存在
在 设备/网络 模块: 建立连接, 发数据, 收数据(不管数据是啥,只做收发)
在 UI 中: 某些具备基础功能的控件, 比如带节流的按钮, 比如实现了某个特效的进度条

简述

代码中有很多复杂的逻辑判断,状态管理,业务操作。 但追溯到最里程,始终有一部分功能可以 不含逻辑条件判断,只做最基本最单一的功能实现
是代码层级,逻辑结构等中的最末端节点

实现时,这个层级,绝大部分只依赖系统 API,对外提供最简单的单一操作

最终得到的结果是: 一堆单一操作, 可根据需要任意拼装,任意延伸 的基础模块

通用功能

在文件操作中: 我们时常需要一些便利操作, 比如在上层想只调一个接口,就将文件从 A 移动到 B
需要有角色负责实现: 查源路径 -> 查目标路径 -> 根据需要创建目录 -> 执行移动 这些基础功能的拼装

在通信模块中: 我们在底层收到了一堆 16 进制数据,这些数据 “面向对象的程序” 是不认识的
需要有一个角色承担翻译职责:将从底层收到的数据,翻译成 回调/对象。 将上层给底层发的 Object,翻译成 16 进制包,并装上通信协议

在 UI 设计中: 我们会见到很多很多类似,但又不完全一样的控件。 比如导航栏, 通常都具有【左按钮 | 标题/图片 | 右按钮】这样的结构
这样的控件, 主要实现的是 ”布局结构逻辑“,对外暴露 ”样式和类型“ 控制参数。

简述

这一层怎么来的? 需求中要求实现某个业务,而这个业务需要的 能力,经常会发现在很多业务中都需要,但是细节各有不同
(比如导航栏, 每个页面都有, 每个页面都不同)
我自己的角度, 最初的出发点,是为了复用, 封装一个通用的东西,让其可以在项目多个地方都能用到。
在项目做多了后, 发现基本上 所有业务层功能, 剥离开其所处的环境(状态), 都可以抽象成一个功能
区别只在于,有些东西通用性很广, 有些可能只有此情此景能够使用,抽出来意义不大。

所以现在做业务都会先想一想 这个是不是能剥离业务抽成通用的功能

应该怎么实现:

  • 需要底层支撑, 不直接实现功能, 而是对底层功能的进一步使用(几个步骤整合或者进一步处理)
    比如几个底层接口拼装成流程, 比如对底层收到的 16 进制数据,做解析
  • 不绑定业务, 不包含状态, 而是通过对外提供接口的方式。 供需求方自行设置
    比如移动文件功能, 不指定具体的路径; 数据解析功能,只做解析,不规定每个字段的值

最终得到的结果是: 不绑定业务, 可以在任意具有相同功能的 模块/APP 中使用

业务封装

在文件管理器中, 有一层约定了当前 APP 需要的目录(日志路径,数据库路径,图片缓存路径,下载临时路径), 涉及的文件类型。
外界想要执行文件操作,但又不愿意关心细节,这时就提供: getDownloadPath,saveMedia,cleanLog 等与 当前 APP 强绑定 又隐藏具体操作细节的函数接口

在 UI 中,Component 在业务这一层,已经调用底层通用模板,并设置好了具体的图片,文字,样式。Page 直接放置 Component ,只需要关心 Component 在这个层级的位置大小,而无需在意其他样式参数设置问题

<HomePage>  
	<HomeNavigationBar/>  
	<HomeList/>  
	<HomeButton/>  
</HomePage>  

在通信模块中, 比如上层界面里,有个录制视频按钮(发送消息,让飞机录视频),这个功能需要知道当前分辨率,当前帧数,是否已经开始录像。
但调用的地方是 UI 的 callback,不想在 ui 层关心这些细节,只希望执行录像操作,就可以提供一个接口startVideo, 接口的实现里,判断是否已开始录像,自动从当前的全局状态中获取分辨率,帧率。 并调用底层接口实现录制功能

简述

上面描述的场景,都是在执行某个操作, 这个操作本身跟现在的 APP, 此时此刻的状态息息相关。
这些就称之为 业务封装
OC 中,这部分内容我们倾向于放在 MVC 中 Controller 这一层。 在开发场景多了后,
一是发现: Controller 随着业务增多越来越重, 再好的代码也很难避免一个类超过千行
另一个是: Controller 中关系混乱, 很多业务不是某个 Controller 独有, Controller 包含了不同的业务
这时,就会采取很多 Manager 来代替
再后来,逐渐改进, Manager 命名的东西变少,多了 ServicesHelperxxxer 各种不同功能的主题
Controller 层(或者说 React 中的 Page)内容越来越变成中间调度角色

应该怎么实现

  • 针对具体的业务封装, 内部实现 功能 + 当前 APP 业务 + 此刻状态
    比如根据这一款 APP 所有的文件类型,抽象成对应 enum、接口, 在内部指定好路径。 对外直接提供业务接口
    比如对飞机的控制,在结合当前飞机状态的情况下,调用底层提供的接口实现

  • 只实现 一种业务, 可能对应 多个下层模块
    比如飞机拍完照后,APP 下载图片,会涉及到:收回调,请求数据,下载数据,写入数据库,写入磁盘,写入系统相册
    下载流程的业务类,可能会调用很多的底层功能实现目的。 但虽然他和拍照发生在同一个页面,却不能把那块业务也写一块。

最终会得到一个 只能用于当前 APP与当前状态绑定实现单一业务, 但给整体调用提供了极大便捷的类。

参考:架构整洁之道, 看这一篇就够了!-阿里云开发者社区