背景
技术背景
23 年客户端提出了客户端容器化的架构,整个 23 年都是围绕着跨平台基础层和平台基础层进行建设。
经过一年时间的推进,基本上达成了下面的成果:
模块 | 目标 | 业务接入情况 | 我司业务覆盖率 | |
---|---|---|---|---|
基础能力层 | 长链接 | 我司全部长链接都使用跨平台长链接组件 | 接入 Push、wChat 、matchRamdom、世界服、花园、冰饮、match等业务场景 | 75% |
短链接 | 新增业务都采用跨平台短链接组件 | 接入平台化 Passport 业务 | 1% | |
数据库 | IM 业务全部使用 | 已接入上线 | 100% | |
DCS | 我司日志服务全部接入到 DCS | 已接入长链接、短链接、图片库、开放平台协议等业务 | 50% | |
Rust 核心业务层 | Rust IM 业务 | 使用 Rust IM进行数据相关处理,原生提供 UI 刷新和交互逻辑 | 已接入上线 | 100% |
开放平台 | 开放平台双公用业务逻辑都采用 Rust 开发 | 已接入上线 | 100% |
在推进过程中发现的问题:
- 跨平台业务层建设薄弱,现阶段并没有探索出合适的跨平台业务建设的技术方案
- 核心业务场景问题追踪比较困难
业务背景
- 新的娱乐业务场景,交互更加复杂,客户端需要提供统一交互能力
- 新的平台支持,需要跨平台业务层能力更厚实,让上层业务可快速移植到不同平台
新目标
为了解决在跨平台技术推进过程中遇到的问题,24 年客户端主要围绕下面两个部分进行建设:
- 深化跨平台技术建设,探索出适合我司客户端的跨平台业务技术方案,加快推进跨平台业务落地
- 配合全链路追踪,建设完成客户端业务链路追踪 下面主要介绍如何深化跨平台技术建设,追踪相关的规划参考 可观测性 24 年年度规划 。
跨平台技术架构
架构简介
架构演进方向
旧架构仅从分层概念上进行容器化拆分,对于具体的实施方案并没有很好的跟我司的实际业务情况与后续需求进行结合。 旧架构依旧集中在分层概念上,将不同平台定义为某一层级的抽象,这种思路不同平台间的调用关系被视为是对等的,系统提供的平台作为最基础的底层作为整个应用的支撑,整体的构建逻辑还是依赖于具体的开发平台。而新架构将彻底引入 DDD 的思路,通过以业务领域为核心的南北网关调用方向的反向依赖设置,将基础设施抛在外部,对于业务落地来说,具体使用了什么样的底层技术解决方案并不重要。 以聊天业务为例,最核心的部分是聊天领域内的领域建模,如消息类型、信令类型(CMD 指令)、动作类型(收发订阅)等基础操作,这部分是除非产品逻辑大改之外不会去动的部分,在新架构上这部分将作为纯建模代码存在;然后业务层代码将纯建模逻辑进行扩充,通过操作例如消息接口(构建、校验、存储、读取等操作)、信令接口、动作接口等进行实际调用;在下层进行具体的基础实现,如 mqtt 协议处理、网络处理等;在上层进行 UI 事件绑定与状态控制等。 由于在新架构下改变了依赖关系,并且以领域为核心的部分将全部由 Rust 来编写,实现业务逻辑层面的多端完全统一,因此最上层与最底层的实现方式将变得更加灵活,可以随时替换上层的 APP UI 实与最底层的 APP 平台框架。 在未来,我司所有以新架构为思路构建的应用,都可以快速移植到任何开发平台,无论是桌面端、移动端还是网页端,都可以进行快速构建发版。
分层菱形对称架构
- 表现层:用户与应用程序交互的界面,主要职责是向用户展示数据并处理用户的输入。
- 视图(App UI):可以采用多种技术和框架实现,如 SwiftUI、Jetpack Compose、ArkUI(HarmonyOS)、Flutter、React Native、Weex 等
- 容器(Containers):WebKit、Cocos Engine、Unity Engine 等
- 应用层:
- 控制器(Controller):不包含任何业务逻辑,是与外界交互的控制层,处理与用户界面、用户输入、外部服务的交互。
- 应用业务(AppBiz):定义应用应该完成的工作,协调应用活动并向工作委托给下一层。这些逻辑是由对领域层的调用组成的,而不包含业务规则本身
- 领域层:负责代表业务概念。自成一体,不依赖于任何其他层。领域层应该与其他层良好隔离。
- 领域模型(Domain):核心业务领域模型,负责表示和封装业务领域内的概念、业务规则、策略和逻辑。
- 接口(Interface):各种通用技术能力的抽象接口,比如网络、持久化、设备、传感器等。被 Domain 层依赖,被 Adapter 层实现。
- 基础设施层:
- 适配器(Adapter):领域层接口的实现。依赖外部基础层,抹平平台差异。通过依赖注入将适配器实现注入到领域层
- 外部基础层:
- 跨平台能力组件(Cross-Platform Libraries):使用跨平台语言开发,如 Rust、C++ 等,提供 Socket、HTTP、SQLite、DCS 等跨平台能力
- 原生能力组件(Native System Libraries):由系统提供的与设备有关系的系统功能库。具有平台差异性,需要在各个平台上实现。比如相册相机、定位、传感器、应用代理等
领域层和应用层业务的区别:领域层集中于“业务是什么”(即业务规则和逻辑的实现),而应用层关注“业务如何进行”(即如何使用领域模型来实现特定的业务操作或用例)。领域层的设计更专注于业务本身的复杂性,而应用层则涉及数据流转和任务协调。
开发语言
在架构图中,可划分为上、中、下三部分
- 上部分:表现层,可以采用多种技术和框架来开发,不限制开发语言
- 中部分:中间菱形部分,全部采用 Rust 语言开发,充分利用 Rust 语言的优势。构建健壮且高效的跨平台应用核心。
- 下部分:外部基础层,根据功能的不同,可以采用不同的语言开发。优先采用 Rust 语言开发。iOS 端采用 Swift,Android 使用 Kotlin。
上部分中间部分通过
星桥
通讯;中间部分和下部分通过直接依赖或 cxx、FFI、UniFFI 来交互。后面技术实现
部分会详细介绍
组件代码迭代
如架构图右侧所示,从下至上,组件代码的迭代频率升高。 越往下层,组件提供的功能更独立、更纯粹且稳定。非系统不兼容性升级,一般不会修改相关组件代码。 越往上层,组件提供的功能更偏向具体业务、要更灵活,需要能高效的实现业务功能。会随产品功能的迭代而频繁修改。
自动化测试
在 DDD 中引入自动化测试是一个重要的步骤。 可以带来多种收益,这些收益不仅限于提高代码质量,还涉及到整个软件开发生命周期的各个方面:
1. 提高代码质量和可靠性:
- 自动化测试通过持续验证代码的正确性,帮助确保业务能力的正确性。
- 可以减少缺陷和归回次数,提高系统的稳定性和可用性。
2. 重构和长期维护性:
- 自动化测试提供了一个安全网,让重构和代码维护变得更加安全。修改代码时,不用再担心因代码是复杂的旧逻辑,而不敢去修改。
- 当修改或扩展功能时,可以快速验证更改会不会破坏现在能力。
3. 增强对领域模型的理解:
- 测试案例本身可以作为领域模型和业务规则的文档化。
- 若引入测试驱动的开发方法(TDD),会促使开发人员深入思考领域规则和业务逻辑,从而增强对领域模型的理解。
4. 加速开发和测试过程:
- 减少手动测试的需要,加快反馈循环,使得问题可以更早被发现和解决。
- 支持 CI/CD,实现快速迭代。
5. 降低项目风险:
- 可以及早发现缺陷,减少生产环境下的问题,降低业务风险。
- 对于复杂的业务场景,自动化测试提供了稳定性保障。
6. 促进团队协作和沟通:
- 测试作为沟通工具,帮助团队成员(包括开发人员和非技术人员)对业务规则和行为达成一致理解。
- 提高跨团队(如开发、QA、产品)的协作效率。
7. 提高开发者正反馈和信心:
- 可以作为评估开发质量的一种方式。
- 通过持续的测试结果,及时的测试反馈,可以对相关开发者提高对业务开发信心。可以更放心的去修改、重构优化代码。可以有效防止代码“屎”化。
8. 优化资源分配:
- 自动化测试减少了对手动测试资源的需求,使团队能够将精力集中在更具挑战性和价值的任务上。
总之,在 DDD 中引入自动化测试不仅是提高软件质量的技术手段,也能提升整体项目管理效率、增强团队协作能力、降低业务风险和加快业务响应迭代速度。
领域层测试
领域层是业务核心,需要保证其稳定性。领域层的自动化测试是必须的。 在基础设施层,适配器(Adapter)是领域层接口的实现。为了测试领域层输入输出一致性,加入 Mock Implementation 提供模拟执行结果。
![领域层测试](/media/24fronttech/eeb8be95-5dcc-45cd-9da9-dc77f5f5a3dc.png)
![领域层测试](/media/24fronttech/64e006d3-5084-40d9-ad9c-51b13965ae1a.png)
![注入 Adapter](/media/24fronttech/85c82085-5873-488c-a22c-1909439da346.png)
在实施自动化测试时,确保测试具有足够的覆盖率,能够覆盖领域模型的各个方面,并且测试结果可预测且一致。并且测试代码易于维护,随着领域模型的演进而更新。
无头测试
除表现层 GUI 外,测试应用的所有功能。通过数据驱动测试、结构化测试、模糊测试等手段,进行边界条件、异常情况等,全方位测试。
技术实现方案
事件驱动框架
借鉴 Elm 架构(The Elm Architecture, TEA),设计符合我们业务开发的事件驱动框架。
TEA 基础模式如下:
- 用户在 view 上的操作 (比如按下某个按钮),将会以消息的方式进行发送。Elm 中的某种机制将捕获到这个消息。
- 在检测到新消息到来时,它会和当前的 Model 一并,作为输入传递给 update 函数。这个函数通常是 app 开发者所需要花费时间最长的部分,它控制了整个 app 状态的变化。作为 Elm 架构的核心,它需要根据输入的消息和状态,演算出新的 Model。
- 这个新的 model 将替换掉原有的 model,并准备在下一个 msg 到来时,再次重复上面的过程,去获取新的状态。
- Elm 运行时负责在得到新 Model 后调用 view 函数,渲染出结果 (在 Elm 的语境下,就是一个前端 HTML 页面)。用户可以通过它再次发送新的消息,重复上面的循环。 我们在基础模式上进行演进。
Rust 服务池
对业务规范化状态管理和生命周期管理。包括创建、初始化、暂停、持久化、恢复和销毁等阶段。 相比于传统的单例模式来管理一些常驻的服务有以下优点:
- 更好的模块化和解耦:服务池可以更灵活地管理服务实例,减少服务之间的耦合。不同的组件可以根据需要请求服务,而无需知道服务的具体实现。
- 更易于测试和维护:由于服务池提高了代码的模块化,它使得单元测试和维护变得更加容易。可以独立地测试单个服务,而不需要关心整个单例的状态。
- 动态生命周期管理:服务池可以根据应用程序的当前状态和需求动态创建和销毁服务实例。可以更好地管理资源,尤其是在资源受限的移动环境中。
- 灵活的资源管理:服务池可以根据应用程序的需要动态地暂停或销毁低优先级服务,这有助于更高效地利用内存和处理能力。
- 更好的并发控制:在多线程环境中,服务池可以提供更细粒度的控制,帮助管理并发访问和资源共享。 虽然使用服务池有许多优点,但也有一些缺点,需要在设计和开发中更好的规范:
- 复杂性增加:相比于简单的单例模式,服务池的实现和管理通常更复杂。需要更多地关注池的创建、管理、资源回收等细节。
- 性能开销:如果服务池的管理机制没有得当,可能会引入额外的性能开销,例如频繁地创建和销毁服务实例。
- 资源管理风险:不当的服务池实现可能导致资源泄漏或者服务实例的过度创建,如没有及时回收不再使用的服务,或内存泄漏导致服务释放异常等。
- 线程安全问题:在多线程环境下,如果服务没有正确处理同步和并发,可能会引入线程安全问题。
- 依赖管理:服务之间的依赖关系可能变得更加复杂,需要谨慎处理服务间的依赖和顺序问题。
状态同步器
状态同步是指对值(例如 App 前后台状态值、配置参数等值)进行同步。 状态同步方向是从值产生源头,同步至其他相关服务中(如 Rust 服务池、多进程)。且数据不会反方向同步。 状态同步器需要考虑以下几个方面:
- 状态数据结构:定义一个清晰的数据结构来表示要同步的状态。是在不同语言间共通的,类似 ALPC 协议。
- 线程/进程安全:在多线程/进程中,数据的一致性和安全变的尤为重要,确保使用适当的同步机制来管理数据读写(读写锁、文件锁等),避免竞争条件和死锁。
- 更新通知机制:在状态发生变化时,需要通知各个语言平台进行更新。可以通过回调函数、事件监听或观察者模式等实现。
- 错误处理和异常管理:设计错误处理机制,如跨语言边界错误、异常传递等。
- 试和验证:对状态同步器进行彻底的测试,单元测试和跨平台集成测试。
星桥
对星桥增加 Streaming 能力。为事件驱动框架提供通信能力,沟通表现层与应用层。
数据编码
随着 Rust 的发展,serde 已成熟,出现了很多新型的数据(反)序列化组件库。例如最初由 Meta 主导开发的 serde-generate,支持 Bincode 或 BCS 格式,性能更好的。目前该组件已支持以下语言:
- C++
- Java
- Python
- Rust
- Go
- C#
- Swift
- OCaml
- Typescript (in progress)
- Dart (in progress)
由于 Objective-C 语言已不是 Apple 首选开发语言,一些较新的序列化库,均无 OC 版本,需要自己开发支持 OC。
新的业务开发流程
在 DDD(领域驱动设计)中,软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前,通常需要进行大量的业务知识梳理,而后到达软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念。 在初期搭建阶段应严格遵守标准的 DDD 开发流程(如《领域驱动设计模式、原理与实践》)进行,设计和构建领域模型。确保项目不仅符合技术需求,更重要的是能够满足业务需求;有更好的可维护性和可扩展性,为未来的变化和增长奠定基础。 在日常业务开发中,可以简单划分为两类:
- 新增业务:
- 先从领域层实现业务本身规则和逻辑(定义领域模型,制定界限上下文,实现领域模型);
- 若已有基础设施层,提供的能力,无法满足新业务,则需实现相关 Adapter;
- 实现应用层及表现层相关功能;
- 进行测试和验证。
- 业务迭代:
- 分析需求的核心功能,识别出主要的应用层的相关实现,判断哪些是应用层的职责,哪些是领域层的职责,是否会涉及领域层的修改;
- 若不涉及领域层,则在表现层和应用层开发,就可以完成需求开发。
- 若涉及领域层,则可以让熟悉该业务的开发,在领域层提供相关功能接口支持。
相关资料
- Event Sourcing:https://martinfowler.com/eaaDev/EventSourcing.html
- Event-Driven Architecture: The Definitive Guide:https://appmaster.io/blog/event-driven-architecture
- The Elm Architecture:https://guide.elm-lang.org/architecture/
- The Composable Architecture:https://github.com/pointfreeco/swift-composable-architecture
- TCA SwiftUI:https://onevcat.com/2021/12/tca-1/
- Cross-platform app development in Rust:https://github.com/redbadger/crux
- serde-generate:https://github.com/zefchain/serde-reflection
- Bincode:https://github.com/bincode-org/bincode
- BCS:https://github.com/diem/bcs