在现代移动应用开发的宏大叙事中,UI 框架的演进往往最能吸引开发者的目光。HarmonyOS 的 ArkUI 以其简洁的声明式语法和数据驱动的特性,极大地降低了界面构建的门槛。然而,这种极其便利的视图层绑定机制,也悄悄埋下了一个架构陷阱:"UI 驱动开发(UI-Driven Development)" 。

当开发者习惯于在界面的 build() 函数旁直接发起数据库查询,在页面的生命周期中直接编写业务清洗逻辑时,应用的"核心资产"------业务规则,便会被无情地打碎,散落在数百个 .ets 文件中。随着业务的迭代,这些代码将迅速腐化,变得难以测试、无法复用,最终成为阻碍项目演进的技术债。
为了在 HarmonyOS 环境下构建一个具有长生命周期的复杂应用,我们需要将目光从绚丽的 UI 移开,重新聚焦于软件工程的核心:领域(Domain)。
本文将以情绪追踪应用为切入点,深度剖析如何将领域驱动设计(Domain-Driven Design, DDD)的哲学引入 ArkTS 工程,剥离底层的持久化细节与上层的视图状态,打造一个高内聚、零耦合、纯粹基于 TypeScript/ArkTS 语言特性的核心业务底座。
一、破除"贫血模型":让实体重新掌握业务主权
在很多传统的 MVC 或 MVVM 项目中,数据模型往往被定义为只有 getter 和 setter(或在 ArkTS 中仅包含公共属性)的简单数据载体,这在领域驱动设计中被称为贫血模型(Anemic Domain Model)。
如果情绪追踪的底座采用贫血模型,它的代码可能长这样:
typescript
// 反面模式:贫血的接口定义
export interface MoodData {
id: string;
score: number;
timestamp: number;
note: string;
}
这种定义下,MoodData 仅仅是一个哑巴数据包。如果我们需要判断这个情绪是否属于"极端低落",或者需要计算它的标准展示日期,这些逻辑就不可避免地会泄漏到 ViewModel 或具体的 UI 组件中。
1.1 充血模型(Rich Domain Model)的构建
在 DDD 的视角下,MoodRecord 不仅仅是一条数据库记录,它是情绪追踪领域中的聚合根(Aggregate Root)。它不仅应当包含数据,更应当封装所有与该数据强相关的业务行为与不变量约束。
在 ArkTS 中,我们利用 class 来构建真正的领域实体:
typescript
import { generateUUID } from '../utils/IdGenerator';
/**
* 情绪记录聚合根 (Aggregate Root)
* 封装了核心情绪数据及其领域行为,绝对不依赖任何 UI 框架或数据库 API
*/
export class MoodRecord {
// 实体标识 (Identity)
private readonly _id: string;
private readonly _timestamp: number;
// 核心领域状态
private _score: number;
private _note: string;
private _tags: Set<string>;
constructor(score: number, note: string = "", timestamp: number = Date.now(), id?: string) {
this.validateScore(score); // 构造时进行防腐校验
this._id = id || generateUUID();
this._timestamp = timestamp;
this._score = score;
this._note = note;
this._tags = new Set<string>();
}
// --- 属性访问器 ---
get id(): string { return this._id; }
get timestamp(): number { return this._timestamp; }
get score(): number { return this._score; }
get note(): string { return this._note; }
get tags(): string[] { return Array.from(this._tags); }
// --- 领域行为 (Domain Behaviors) ---
/**
* 业务规则:情绪净值必须在 -2 到 2 之间
*/
private validateScore(score: number): void {
if (!Number.isInteger(score) || score < -2 || score > 2) {
throw new Error(`领域异常:无效的情绪分值 [${score}]`);
}
}
/**
* 业务行为:更新情绪分值
*/
public reviseScore(newScore: number): void {
this.validateScore(newScore);
this._score = newScore;
}
/**
* 业务行为:添加标签(内聚了去重逻辑)
*/
public addTag(tag: string): void {
const cleanTag = tag.trim();
if (cleanTag.length > 0 && cleanTag.length <= 10) {
this._tags.add(cleanTag);
}
}
/**
* 派生属性:获取标准化的情绪强度级别 (供上层调用,屏蔽内部数值逻辑)
*/
public getIntensityLevel(): 'EXTREME_NEGATIVE' | 'NEGATIVE' | 'NEUTRAL' | 'POSITIVE' | 'EXTREME_POSITIVE' {
switch (this._score) {
case -2: return 'EXTREME_NEGATIVE';
case -1: return 'NEGATIVE';
case 0: return 'NEUTRAL';
case 1: return 'POSITIVE';
case 2: return 'EXTREME_POSITIVE';
default: return 'NEUTRAL';
}
}
}
架构红利分析:
- 状态安全 :通过私有属性(
private)和只读属性(readonly),彻底杜绝了外部代码对id和timestamp的非法篡改。 - 规则内聚 :
validateScore方法将业务规则死死地钉在了实体内部。无论是在新建记录页面,还是在后台自动同步模块,只要实例化MoodRecord,就必然受到这套规则的约束。 - 语言纯粹性 :这个类完全是一段纯粹的 TypeScript 代码,没有任何
@State或@ohos.data.relationalStore的导入。这意味着它可以脱离鸿蒙设备,直接在 Node.js 环境下使用 Jest 进行毫秒级的单元测试。
二、隔离存储细节:仓储模式(Repository Pattern)的落地
在定义了纯粹的领域实体后,应用必须面对现实:数据需要被持久化到磁盘上。
在鸿蒙开发中,最常用的本地存储是关系型数据库(RDB, relationalStore)。如果我们在修改了 MoodRecord 后,直接在业务代码里编写 SQL 语句并调用 RDB 接口,那么领域层就严重依赖了基础设施层(Infrastructure Layer)。
一旦未来华为推出了更高效的端侧向量数据库,或者我们需要将数据无缝同步到云端,由于 SQL 逻辑散落各处,重构将极其艰难。
2.1 依赖倒置:定义仓储接口
为了实现零耦合,我们必须遵循依赖倒置原则(Dependency Inversion Principle)。领域层不应该知道数据是如何存储的,它只负责定义"它需要什么存储能力"。
在领域层(Domain Layer)中,我们声明一个仓储接口:
typescript
// domain/repository/IMoodRepository.ets
import { MoodRecord } from '../model/MoodRecord';
/**
* 情绪仓储接口
* 定义了领域实体持久化的契约,领域层仅依赖此接口
*/
export interface IMoodRepository {
/**
* 保存或更新情绪记录
*/
save(record: MoodRecord): Promise<void>;
/**
* 根据 ID 查找记录
*/
findById(id: string): Promise<MoodRecord | null>;
/**
* 获取指定时间范围内的记录
*/
findInRange(startTime: number, endTime: number): Promise<MoodRecord[]>;
/**
* 删除记录
*/
delete(id: string): Promise<void>;
}
2.2 防腐层(ACL)的物理实现:关系型数据库适配器
接口定义完成后,我们在基础设施层(Infrastructure Layer)去具体实现这个接口。这里的实现类充当了防腐层(Anti-Corruption Layer)的角色,它负责将我们优雅的领域实体 MoodRecord,转化为底层的数据库键值对(ValuesBucket)。
typescript
// infrastructure/database/RdbMoodRepository.ets
import relationalStore from '@ohos.data.relationalStore';
import { IMoodRepository } from '../../domain/repository/IMoodRepository';
import { MoodRecord } from '../../domain/model/MoodRecord';
export class RdbMoodRepository implements IMoodRepository {
private rdbStore: relationalStore.RdbStore;
constructor(store: relationalStore.RdbStore) {
this.rdbStore = store;
}
async save(record: MoodRecord): Promise<void> {
// 【防腐层转换】:将领域实体转换为底层 RDB 接受的扁平数据字典
const valueBucket: relationalStore.ValuesBucket = {
'id': record.id,
'timestamp': record.timestamp,
'score': record.score,
'note': record.note,
'tags': JSON.stringify(record.tags) // 将 Set 序列化为 JSON 字符串入库
};
// 使用 RDB 提供的 insert API(若存在则替换)
await this.rdbStore.insert('MoodTable', valueBucket, relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
}
async findById(id: string): Promise<MoodRecord | null> {
let predicates = new relationalStore.RdbPredicates('MoodTable');
predicates.equalTo('id', id);
const resultSet = await this.rdbStore.query(predicates);
if (resultSet.rowCount > 0) {
resultSet.goToFirstRow();
// 【防腐层转换】:将数据库结果集还原为纯粹的领域实体
const score = resultSet.getDouble(resultSet.getColumnIndex('score'));
const note = resultSet.getString(resultSet.getColumnIndex('note'));
const timestamp = resultSet.getLong(resultSet.getColumnIndex('timestamp'));
const tagsJson = resultSet.getString(resultSet.getColumnIndex('tags'));
const record = new MoodRecord(score, note, timestamp, id);
// 恢复 tags
if (tagsJson) {
const tagsArray: string[] = JSON.parse(tagsJson);
tagsArray.forEach(tag => record.addTag(tag));
}
resultSet.close();
return record;
}
resultSet.close();
return null;
}
// ... 实现其他接口方法
}
通过这套接口与实现分离的设计,在 ViewModel 或页面拦截器中,我们只需要注入 IMoodRepository 接口即可。当我们在测试环境下,完全可以轻松替换为一个 InMemoryMoodRepository,在不挂载任何真实 RDB 数据库的情况下完成整个业务流程的验证。
三、跨实体业务的收容所:领域服务(Domain Service)
除了实体自有的行为,情绪追踪应用中还存在着一些跨实体的复杂业务逻辑。例如:"计算用户当前的连续记录天数(Streak)",或者"分析近 7 天的情绪起伏趋势"。
这些逻辑不属于单一的 MoodRecord 实体,也不应该写在 UI 层。在 DDD 架构中,我们将这些无状态(Stateless)的复杂业务过程抽象为领域服务(Domain Service)。
3.1 构建 EmotionAnalysisService

typescript
// domain/service/EmotionAnalysisService.ets
import { MoodRecord } from '../model/MoodRecord';
export class EmotionAnalysisService {
/**
* 领域逻辑:计算连续记录天数
* @param records 用户的历史记录(需按时间倒序排列)
* @returns 连续天数
*/
public static calculateStreak(records: MoodRecord[]): number {
if (!records || records.length === 0) return 0;
let streak = 0;
const now = new Date();
// 抹平时间精度,只取日期部分进行比对
const todayStr = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
let lastRecordedDateStr = "";
for (const record of records) {
const d = new Date(record.timestamp);
const recordDateStr = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
// 第一条记录必须是今天或昨天,否则连胜已断
if (streak === 0) {
// ... 具体的时间差计算逻辑
streak++;
} else {
// 判断是否严格连续
// ...
}
}
return streak;
}
/**
* 领域逻辑:提取核心情绪标签
*/
public static extractDominantTags(records: MoodRecord[]): string[] {
const tagFrequency = new Map<string, number>();
records.forEach(r => {
r.tags.forEach(t => {
tagFrequency.set(t, (tagFrequency.get(t) || 0) + 1);
});
});
// 返回出现频率最高的 Top 3 标签
return Array.from(tagFrequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(entry => entry[0]);
}
}
领域服务的存在,填补了聚合根无法处理宏观逻辑的空白。它们像是一个纯净的计算引擎,接收领域实体的数组,输出高度凝练的业务结论。这些服务函数是纯函数,时间复杂度可控,且对 ArkUI 零感知。
四、事件驱动架构:消灭模块间的硬连接

在复杂的鸿蒙原生应用中,当发生一个核心业务动作(例如:新建了一条情绪记录)时,往往需要触发多个旁路操作:
- 更新本地数据库。
- 触发桌面服务卡片(Widget)的数据刷新。
- 检查是否达成了某个隐藏成就(Achievement)。
- 将记录推入跨设备分布式软总线进行手表端同步。
如果我们在新建记录的 UI 组件里,将这些操作依次写下:
saveDb(); updateWidget(); checkAchievement(); syncToWatch();
那么这段代码就成了一个巨大的耦合灾难,不仅难以维护,而且任何一个旁路操作的失败(比如手表断连)都可能阻塞主线程,导致 UI 卡顿。
4.1 引入领域事件(Domain Event)
为了实现系统级的零耦合,我们需要在领域底座中引入事件驱动机制。当状态改变时,领域层只负责发布事实,而不在乎谁来处理。
首先定义事件契约:
typescript
// domain/event/MoodCreatedEvent.ets
import { MoodRecord } from '../model/MoodRecord';
/**
* 领域事件:新的情绪记录已创建
*/
export class MoodCreatedEvent {
public static readonly EVENT_NAME = 'DOMAIN_EVENT_MOOD_CREATED';
public readonly record: MoodRecord;
public readonly occurredOn: number;
constructor(record: MoodRecord) {
this.record = record;
this.occurredOn = Date.now();
}
}
4.2 利用鸿蒙 Emitter 实现事件总线
我们可以利用鸿蒙系统底层的 @ohos.events.emitter 模块,构建一个轻量级的跨模块事件总线(Event Bus)。在应用启动时,各个模块(成就模块、桌面卡片模块)自行订阅感兴趣的事件。
typescript
// application/EventDispatcher.ets
import emitter from '@ohos.events.emitter';
import { MoodCreatedEvent } from '../domain/event/MoodCreatedEvent';
export class EventDispatcher {
public static publishMoodCreated(event: MoodCreatedEvent): void {
const eventData: emitter.EventData = {
data: {
// 由于 Emitter 只能传递简单对象,我们将领域模型序列化传递
recordJson: JSON.stringify(event.record)
}
};
// 定义唯一的事件 ID
const innerEvent: emitter.InnerEvent = { eventId: 1001, priority: emitter.EventPriority.HIGH };
// 广播事件
emitter.emit(innerEvent, eventData);
}
}
此时,在新建记录的用例(Use Case)中,代码变得极其优雅与干脆:
typescript
// application/usecase/CreateMoodUseCase.ets
export class CreateMoodUseCase {
private repository: IMoodRepository;
constructor(repo: IMoodRepository) {
this.repository = repo;
}
async execute(score: number, note: string): Promise<void> {
// 1. 创建领域实体
const newRecord = new MoodRecord(score, note);
// 2. 通过仓储持久化
await this.repository.save(newRecord);
// 3. 发布领域事件,彻底解耦旁路逻辑
const event = new MoodCreatedEvent(newRecord);
EventDispatcher.publishMoodCreated(event);
}
}
通过事件驱动架构,成就模块和系统卡片模块成为了被动的"订阅者"。它们可以在自己独立的 Worker 线程或后台任务中去消化这些事件,从而保障了主界面交互的绝对流畅。
结语:在规则的约束下重获自由
构建一个高内聚、零耦合的鸿蒙应用模型,本质上是一场对抗软件熵增的持久战。
从充血的聚合根 MoodRecord 开始,我们夺回了分散在 UI 中的业务校验权;通过 IMoodRepository 的抽象,我们斩断了底层数据库 API 对核心业务的物理绑架;借助纯粹的 Domain Service,我们收编了跨实体的复杂计算;最终,通过领域事件的发布,我们实现了模块间的彻底松绑。
在这套严密的领域驱动视角下,ArkUI 终于可以回归其最本真的使命:成为一组纯粹、高效的数据渲染引擎。当底盘极其稳固时,无论未来的鸿蒙生态是向折叠屏、智能手表还是跨端 AI 演进,这座建立在坚实领域模型之上的大厦,都将拥有无尽的生长空间与不可撼动的稳定性。