整洁架构

我们的 App 遵循整洁架构open in new window

整洁架构是 Uncle Bob 的心血结晶,他是《代码整洁之道》的作者。这种架构的要义是,业务逻辑(也称为 Domain),是应用的核心。

依赖规则

在整洁架构的世界里,我们将不同职责的类,分派到不同的圈层中。

外层应当依赖内层。

外层看见,知道,了解内层,但内层看不见,也不知道,更不了解外层。

clean-architecture-2021-10-15-01-52-12

内层是业务层,与 UI 无关,与设备无关,它们不依赖任何 React 以及 React Native 的东西,它们不知道自己运行在哪个环境中,即使将它们的代码原封不动地拷贝到一个小程序项目中,只要外层实现了内层的接口,代码就能正常运转。

UI 在外层,数据也在外层,UI 需要展示数据,但 UI 层和数据层不能彼此依赖,它们都依赖业务层,那数据是如何流向 UI 的呢?

答案是控制反转open in new window

业务层声明自己所依赖的数据接口,数据层实现这些接口,我们通过依赖注入,向业务层注入数据接口的实现。

业务层

我们遵循高内聚,低耦合的原则,把相关的 UseCase 都聚合到同一个 Service 中,不同的 Service 管理着各自的领域。

譬如 AuthService 管理着和身份认证相关的用例,包含登入、登出、刷新 token、第三方登录等逻辑。

业务层是整个应用的核心,UI 层和数据层都依赖它。

如果业务层的业务状态发生变化,如何通知 UI 层呢?

我们通过订阅 / 发布模式来实现这点。

一个事件只能由同一个 Service 发布,确保单一事件源。

数据层

我们通过 Repository Patternopen in new window 屏蔽数据的来源,使用者不需要关心数据是来源于网络还是缓存。

Repository 需要遵循单一可信来源open in new window原则。

我们在内层定义 Repository 接口,Service 依赖这些接口。

我们在外层实现这些 Repository 接口,最后通过依赖注入的方式,为 Service 提供 Repository 的具体实现。

但是 Service 对 Repository 的实现一无所知,因为它依赖的是接口。

数据层从后台或本地存储中获取到的数据,通常是 json,或者 key-value。这些数据不能直接传递给服务层,需要转换成服务层所使用的数据结构。

设备

设备位于最外层。

假如我们的应用需要定位功能,按照整洁架构的依赖原则,我们的设备依赖我们的业务层。

MVC, MVP, MVVM

MVC, MVP, MVVM 都是表现层的架构。

Controller, Presenter, ViewModel 是连接业务层和 UI 的桥梁,它们负责处理 UI 状态逻辑。

React Hookopen in new window 承担了表现层部分状态逻辑。

UI 层从服务层所获得的数据,有时候,或大多数情况下,并不能直接作为界面的状态展示出来,需要先进行适配。这也是为什么绿色那层也被称为接口适配层的原因。譬如,我们从服务层拿到的表示时间的数据,可能是一个整型的时间戳,需要先适配成描述性的字符串后展示出来。

有时,一个 UI 界面所依赖的数据来自于多个 Service,我们需要把这多个数据源聚合在一起,转换(适配)成 UI 可以展示的数据结构。譬如一个用户头像组件,它的头像数据来源于 UserService,然而头像的角落有个 Badge 来表示未读的消息数目,这个数据则来源于 ChatService,我们需要组合这些数据,构造一个叫 AvatarItem 的数据结构,传递给 AvatarComponent

interface AvatarItem {
  url: string
  unread: number
}

UI 层的数据转换逻辑放在哪呢?如果采用了 MVP 或者 MVVM 模式,就放在 Presenter 或 ViewModel 中,否则就放在和 UI Component 相同的文件中,可以是一个工具函数,也可以是一个方法。工具函数如果需要多处复用,则抽取到 helper 文件中。

学习参考资料

浅谈 MVC、MVP 和 MVVM 架构模式open in new window

Socket

(Web)Socket 和设备一样,位于最外层。

当服务器通过 Socket 给 App 发送信息时,相当于用户操作了界面。此时,处理该消息的 Handler 就是控制器,它们依赖 Service,调用 Service 提供的方法操作数据。

当 Service 需要通过 Socket 给服务器发送消息时,Socket 就相当于 Repository,此时,应当遵循 Repository 模式。Service 依赖 Repository 接口,而 Socket 层则实现这一接口。

异常处理

我们在业务层定义了若干常见异常(Error)。

所有的网络访问 Error,都应当转译成业务 Error,因为业务层不依赖网络层,更不能把网络异常传递给 UI 层。

转译后的 Error 应当包含原始的 Error

class WrappedError extends Error {
  cause: Error | null
  constructor(cause: Error | null, message?: string) {
    super(message)
    this.cause = cause
  }
}

export class AuthError extends WrappedError {
  constructor(cause: Error | null, message: string = "手机号码或验证码错误") {
    super(cause, message)
    this.name = "AuthError"
  }
}

业务层通常不捕获异常。

UI 层则应当捕获业务层抛出的 Error,提示用户操作失败,或提供重试机会。

UI 层在处理异常时,可以使用 Result Pattern,我们提供了一个辅助方法来帮助使用该模式

export async function result<T, U = Error>(promise: Promise<T>): Promise<[U | null, T | null]> {
  try {
    const data = await promise
    return [null, data]
  } catch (err) {
    return [err, null]
  }
}

但是,不能强迫客户代码使用该模式。也就是说,只应该在方法内部处理异常时使用 Result Pattern,不能将该模式用于方法的返回值中,而是应该抛出异常。

上次更新: 6/30/2022, 6:52:25 AM