架构困境与四层结构化设计

Neo 框架连载 · 第一篇 · AI 辅助撰写

在 AI 编程工具快速普及的今天,产品的试错成本大幅降低——把 IDEA 尽可能快地做出来才是最重要的,人工打磨细节和文章的 ROI 已经不高了。本系列文章均为人指导、AI 生成的内容,核心思路和设计决策来自人的判断,AI 负责快速落地。

软件工程不止编码

在鸿蒙应用开发过程中,工作内容远不止写代码。需求评审、三方 SDK 对接(鸿蒙化进度不受控时需要兜底方案)、功能测试、自动化测试、稳定性测试、CI/CD 部署……这些都是日常。

这些环节的质量,很大程度上取决于系统的初始设计。

平铺开发的典型症状

一般来说,组织沟通方式会通过系统设计表达出来——康威定律。基于 Spring 的微服务是特例,技术栈自带局部架构。但客户端不一样,尤其是鸿蒙客户端——技术栈太新,没有"自带架构"的框架可用。

没有明确设计的系统,功能基本是平铺开发,整体结构不超过三层:

  • 层层耦合,处处不内聚 — 页面里直接写网络请求,网络回调里直接操作 UI
  • 看似面向对象,实则面向过程 — 定义了 class,但方法之间是线性调用关系,没有职责划分
  • 补丁叠补丁 — 新需求来了,再加一层 if-else,再补一个回调
  • 霰弹式修改 — 一个业务变更要改七八个文件,每个文件改一两行

项目像一个穿着"百衲衣"的大胖子,某处破裂贴上胶布继续凑合用。按照 Martin Fowler 在《企业应用架构模式》中的观点:随着领域逻辑复杂度的提升,领域建模程度较低的项目,增加的工作量是近似指数级的。

三个客观现实:

  1. 紧迫的任务与受限的预算 — 中小团队没有时间做"理想"的重构
  2. 开发团队较低的组织程度和建模意识 — 没有框架约束,每人按自己的理解写代码
  3. 增加的工作量越来越不受控 — 前两者叠加的必然结果

前些年很火的 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 示例为例:

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 如何实现模块化服务编排与渐进式启动。


系列文章

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐