架构困境与四层结构化设计
Neo 框架连载 · 第一篇 · AI 辅助撰写
在 AI 编程工具快速普及的今天,产品的试错成本大幅降低------把 IDEA 尽可能快地做出来才是最重要的,人工打磨细节和文章的 ROI 已经不高了。本系列文章均为人指导、AI 生成的内容,核心思路和设计决策来自人的判断,AI 负责快速落地。
软件工程不止编码
在鸿蒙应用开发过程中,工作内容远不止写代码。需求评审、三方 SDK 对接(鸿蒙化进度不受控时需要兜底方案)、功能测试、自动化测试、稳定性测试、CI/CD 部署......这些都是日常。
这些环节的质量,很大程度上取决于系统的初始设计。
平铺开发的典型症状
一般来说,组织沟通方式会通过系统设计表达出来------康威定律。基于 Spring 的微服务是特例,技术栈自带局部架构。但客户端不一样,尤其是鸿蒙客户端------技术栈太新,没有"自带架构"的框架可用。
没有明确设计的系统,功能基本是平铺开发,整体结构不超过三层:
- 层层耦合,处处不内聚 --- 页面里直接写网络请求,网络回调里直接操作 UI
- 看似面向对象,实则面向过程 --- 定义了 class,但方法之间是线性调用关系,没有职责划分
- 补丁叠补丁 --- 新需求来了,再加一层 if-else,再补一个回调
- 霰弹式修改 --- 一个业务变更要改七八个文件,每个文件改一两行
项目像一个穿着"百衲衣"的大胖子,某处破裂贴上胶布继续凑合用。按照 Martin Fowler 在《企业应用架构模式》中的观点:随着领域逻辑复杂度的提升,领域建模程度较低的项目,增加的工作量是近似指数级的。
三个客观现实:
- 紧迫的任务与受限的预算 --- 中小团队没有时间做"理想"的重构
- 开发团队较低的组织程度和建模意识 --- 没有框架约束,每人按自己的理解写代码
- 增加的工作量越来越不受控 --- 前两者叠加的必然结果
前些年很火的 DDD(领域驱动建模),在中小团队中培训成本高到不可能落地。但我认为最适合领域建模的软件产品是客户端而非服务器------客户端所有代码跑在同一个进程里,没有网络边界作为天然屏障,一个烂模块会影响整个应用。
两种约束
面对以上问题,我提出两种约束------不是"最佳实践",而是划定底线:
- 约束一:相对完整的结构化设计,不能有过高的理解成本
- 约束二:功能模块的规范定义,编排层次性的启动顺序
这两种约束的具体落地就是 Neo 框架。下面展开约束一。
四层结构化架构
┌─────────────────────────────────────┐
│ entry(应用入口) │
│ 页面入口 / 路由 / 一多方案适配 │
├─────────────────────────────────────┤
│ features(功能页面层) │
│ 只处理页面交互,不处理数据逻辑 │
├─────────────────────────────────────┤
│ domains(领域建模层) │
│ 数据获取、业务逻辑、跨功能服务 │
├─────────────────────────────────────┤
│ infra(基础设施层) │
│ 无状态,可迁移,三方 SDK │
└─────────────────────────────────────┘
entry --- 应用入口
功能聚集层。入口页面、路由、一多方案适配,既是所有页面和功能的门面,也是构建的集合。按照项目实际情况可以选择一多方案适配或多端独立方式适配。
features --- 功能模块页面层
通过领域建模获取数据,自身只处理页面交互逻辑,不处理服务器、硬盘的数据。上层页面是相对抽象的,聚合内部功能的;下层负责具体功能。
domains --- 领域建模层
这里并不一定要使用领域驱动建模的概念。具体业务领域是容易区分的,但公共能力很容易渗透到全局。上层的责任是编排各个领域,而非公共能力。
例如用户鉴权,很容易被拆分成用户数据结构在最下层、登录逻辑在上层。而更合理的情况是:上层有自己的值对象,登录鉴权的逻辑和数据结构内聚,完成这个过程后通知全局,自上而下分发状态。
infra --- 基础设施层
最简单的理解就是当做二方和三方,尽可能按照可迁移到其他项目的思路设计。重点考虑无状态和可迁移------不是真的要迁移,而是最干净的基础设施是完全无状态的。
状态指的是由使用、登录获取的数据及其传递依赖。例如从个人登录信息 → 登录会议 SDK → 处理会议数据,这里就是状态的传递。无状态的模块不可以主动获取状态,需要数据应由调用方传入。
层级边界渗透
企业的最初和最终的目的是盈利,项目最初和最终的定位是工具。设计原则不管是 SOLID 还是七原则,都是局部"术"的层面,而真实的世界是混沌的。各层之间的设计都应考虑层级边界渗透的情况。
entry ↔ 下层
页面是相对抽象的,聚合内部功能的,主要是整体页面框架、路由、一多适配。与下层的边界较好区分。
features ↔ 下层
features → domains:页面逻辑还是业务逻辑?通常的经验是页面操作驱动业务逻辑,业务数据驱动页面逻辑。例如网络通话场景,通话状态的转移是 SDK 数据的变化,页面也会根据这个数据变化。但页面不存在通话就不存在了吗?现在的大部分通话场景都已支持悬浮窗,通话的数据要独立在自己的领域建模中。
features → infra:具体功能页面还是组件?某个组件是否需要复用,复用即在下层。
domains ↔ infra
渗透的重灾区。在没有建模的项目中,事实上的基础模块很多都是带业务状态的。这种很难改------改完容易出错,出错容易背锅,写的越多越错。逻辑的编排需要分清是通用逻辑还是业务逻辑,这部分可以适当用一些设计模式。
Neo 的四层在代码中的样子
以 Neo 的 SoulApp 示例为例:
bash
entry/src/main/ets/
├── pages/ # features --- 页面交互
│ ├── IndexPage.ets # 首页
│ ├── ChatPage.ets # 聊天
│ ├── ExplorePage.ets # 发现
│ └── ...
├── services/ # domains --- 领域服务
│ ├── business/ # 核心业务 (12个)
│ │ ├── AuthService # 认证
│ │ ├── ChatService # 聊天
│ │ └── ...
│ ├── feature/ # 功能服务 (5个)
│ └── lazy/ # 非关键服务 (2个)
├── services/infra/ # infra --- 基础设施 (8个)
│ ├── NetworkService
│ ├── DatabaseService
│ ├── CacheService
│ └── ...
├── modules/ # entry --- 模块注册
│ └── AppModule.ets # 所有 Service 的编排入口
└── data/ # 跨层数据模型
├── Models.ets
└── MockData.ets
下一篇将展开约束二:Service、NeoModule、ServiceManager 和 Phase 如何实现模块化服务编排与渐进式启动。
系列文章
- 一种通用中小型基于ArkTS的鸿蒙应用开发框架(华为开发者联盟)
- 01 架构困境与四层结构化设计(本文)
- 02 DI 容器与渐进式启动(即将发布)
- 03 响应式数据流与实战 SoulApp(即将发布)