面向对象 vs 函数式:ArkTS 中的编程范式选择

一、引言
ArkTS 是 HarmonyOS 原生开发语言,它在 TypeScript 基础上增强了静态类型检查和装饰器体系,同时保留了多范式的表达能力。在实际项目开发中,我们经常面临一个选择:用面向对象(OOP)还是函数式(FP)?
这个问题没有绝对答案------每种范式都有其擅长的领域。本文将以我们英语学习 App 的实际代码为例,分析 class + @ObservedV2 的面向对象模型、@Builder 参数化 UI 片段、以及函数式单例工具类(如 AudioPlayer)的设计思路,帮你理清在 ArkTS 中如何做出合理的范式选择。
二、面向对象范式:class + @ObservedV2 响应式模型
2.1 WordCard 数据模型
在 ArkTS V2 装饰器体系中,@ObservedV2 + @Trace 是实现响应式数据模型的标准方式。它的本质是面向对象的------通过 class 封装数据,用装饰器标记可观察属性:
typescript
// commonLib/models/WordCardModel.ets
@ObservedV2
export class WordCard {
@Trace id: number = 0;
@Trace word: string = '';
@Trace phonetic: string = '';
@Trace partOfSpeech: string = '';
@Trace translation: string = '';
@Trace example: string = '';
@Trace exampleTranslation: string = '';
@Trace audioUrl: string = '';
@Trace tags: string[] = [];
@Trace category: string = 'basic';
constructor(id: number, word: string, phonetic: string, partOfSpeech: string,
translation: string, example: string, exampleTranslation: string,
audioUrl: string = '', tags: string[] = [], category: string = 'basic') {
this.id = id;
this.word = word;
// ... 其余字段赋值
}
}
为什么选 class 而非 plain object?
在 ArkTS 中,普通的对象字面量 { word: 'apple', ... } 无法被 @Trace 追踪。只有通过 @ObservedV2 装饰的 class 实例,其 @Trace 属性变化才能触发 UI 刷新。这是面向对象范式在 ArkTS 响应式体系中的核心优势------class 不是可选的,而是响应式的基础。
2.2 @ObservedV2 的工作原理
当你在 @ComponentV2 装饰的组件中读取 wordCard.word 时,ArkTS 框架会自动建立"属性→组件"的依赖关系。一旦 wordCard.word 被修改,只有读取了这个特定字段的组件会重新渲染------其他未依赖的组件不受影响。
typescript
@ComponentV2
struct WordDisplay {
@Param wordCard: WordCard = new WordCard();
build() {
Text(this.wordCard.word) // 只依赖 word 字段
.fontSize(34);
Text(this.wordCard.phonetic) // 只依赖 phonetic 字段
.fontSize(15);
}
}
这种字段级粒度的响应式追踪是面向对象方案独有的能力。如果改用函数式不可变数据,每次修改都需要创建新对象并整体重新赋值,性能开销大且代码更繁琐。
2.3 CourseCoreBar:封装业务逻辑的类
除了数据模型,面向对象也适合封装带有行为的状态:
typescript
// MainPage.ets
@ObservedV2
export class CourseCoreBar {
@Trace showIcon: ResourceStr = '';
@Trace description: string = '';
constructor(showIcon: ResourceStr, description: string) {
this.showIcon = showIcon;
this.description = description;
}
}
这类"带状态的业务对象"天然适合 class------它同时承载数据和 UI 依赖的响应式属性,无需在组件内部维护额外的状态管理逻辑。
三、函数式范式:@Builder 与工具类
3.1 @Builder:参数化 UI 片段
ArkTS 的 @Builder 是一个纯粹的 UI 函数------它接收参数,返回 UI 描述,没有内部状态。这正是函数式编程中"纯函数"的体现:
typescript
// CourseHomePage.ets
@Builder
tabBuilder(label: string, key: string) {
Text(label)
.fontSize(14)
.fontColor(this.currentCategory === key ? '#165DFF' : $r('sys.color.font_secondary'))
.fontWeight(this.currentCategory === key ? FontWeight.Bold : FontWeight.Normal)
.padding({ left: 12, right: 12, top: 6, bottom: 6 });
}
// 在 build() 中调用
TabContent() { this.cardArea(); }
.tabBar(this.tabBuilder('全部', 'all'));
@Builder 的优势在于:
- 无副作用:输入相同参数,输出相同 UI
- 可组合 :多个
@Builder可以嵌套组合 - 按需复用:同一段 UI 逻辑在不同位置调用,只需传不同参数
对于不需要内部状态的 UI 片段,用 @Builder 而非创建一个新组件,可以减少组件树的层次,提升渲染性能。
3.2 Builder 导出模式
项目采用统一的 Builder 导出模式,将每个页面的构建函数暴露给路由系统:
typescript
// CourseHomePage.ets 末尾
@Builder
export function CourseHomePageBuilder() {
CourseHomePage();
}
这是一种"工厂函数"风格------CourseHomePageBuilder 是一个纯函数,每次调用都创建新的组件实例。路由系统通过 RouterTable 统一管理这些 Builder:
typescript
// RouterTable.ets
RouterTable.builderMap.set(RouterMap.WORD_CARD_PAGE, wrapBuilder(WordCardPageBuilder));
这种模式将"创建组件"的职责从组件内部剥离出来,实现了更干净的关注点分离。
3.3 AudioPlayer:单例模式中的函数式设计
AudioPlayer 虽然使用了 class 语法,但它的设计理念更接近函数式------全局唯一实例,无 UI 依赖:
typescript
// commonLib/utils/AudioPlayer.ets
export class AudioPlayer {
private static instance?: AudioPlayer;
private avPlayer: media.AVPlayer | null = null;
private currentUrl: string = '';
private constructor() { }
public static getInstance(): AudioPlayer {
if (!AudioPlayer.instance) {
AudioPlayer.instance = new AudioPlayer();
AudioPlayer.instance.initPlayer();
}
return AudioPlayer.instance;
}
public play(url: string): void {
if (!url || url === '') return;
if (!this.avPlayer) return;
// ... 播放逻辑
}
public stop(): void { /* ... */ }
public release(): void { /* ... */ }
}
工具类使用单例 + 方法的模式有几个优点:
- 无状态副作用 :每次调用
play()只依赖传入的 URL 参数 - 全局复用 :任何一个页面都可以通过
AudioPlayer.getInstance().play(url)播放音频 - 无 UI 耦合:可以在非 UI 线程中使用,不受组件生命周期影响
类似的还有 NewWordManager、GlobalContextUtils 等工具类,都采用同样的单例 + 方法设计。
四、范式对比:何时用哪种?
| 维度 | 面向对象 (class) | 函数式 (@Builder / 单例) |
|---|---|---|
| 状态管理 | @Trace 字段级追踪,适合 UI 数据 |
无状态或全局单例,适合工具能力 |
| 复用方式 | 继承、组合、依赖注入 | 参数化、高阶函数 |
| 响应式 | 原生支持,属性变化自动刷新 UI | 不涉及响应式,适合非 UI 逻辑 |
| 测试难度 | 需要 mock 状态和装饰器环境 | 纯函数测试简单 |
| 典型场景 | 数据模型、ViewModel、业务实体 | UI 片段复用、工具类、路由工厂 |
选择指南
-
数据模型 → 用 class +
@ObservedV2。需要响应式追踪的属性加上@Trace,确保 UI 自动刷新。 -
UI 复用片段 → 用
@Builder。当一段 UI 只是"带参数的模板"且不需要独立生命周期时,@Builder比抽取成独立组件更轻量。 -
工具类/服务类 → 用单例 + 方法。如音频播放、网络请求、数据持久化,这些不依赖 UI 状态的功能适合函数式风格。
-
组件创建工厂 → 用导出函数(Builder 模式)。将组件的创建与路由解耦,方便路由表统一管理。
五、混合范式的最佳实践
实际项目中,两种范式不是互斥的,而是互补的。以下是我们项目中的混合使用模式:
typescript
// 1. 面向对象:ViewModel 管理状态
@ObservedV2
class TopicVM {
@Trace currentIndex: number = 0;
// ... 响应式属性
}
// 2. 函数式:Builder 创建组件
@Builder
export function TopicPageBuilder() {
TopicPage();
}
// 3. 工具类单例:全局服务
const audioPlayer = AudioPlayer.getInstance();
audioPlayer.play('hello.mp3');
// 4. @Builder 复用 UI 片段(在组件内部)
@Builder
commonHeader(title: string) {
Text(title).fontSize(18).fontWeight(FontWeight.Bold);
}
核心原则:有状态的用 class,无状态的用函数。ArkTS V2 的装饰器体系让面向对象在 UI 层如鱼得水,而函数式风格在工具层保持简洁和可测试性。理解两种范式的优劣,在合适的场景选择合适的方式,比固执于一种"纯"范式要实用得多。
六、总结
在 ArkTS 中,面向对象和函数式编程并非"二选一"的对立关系,而是一枚硬币的两面:
- 面向对象 (class +
@ObservedV2/@Trace)是 ArkTS 响应式 UI 的基础,适合数据模型、ViewModel 和业务实体 - 函数式 (
@Builder、单例工具类)在 UI 片段复用、工具服务和无状态逻辑中更简洁 - 混合使用是最佳实践:数据层用 OOP 保证响应式,工具层用 FP 保证可测试性
理解每种范式的设计意图和适用场景,才能写出既符合 ArkTS 平台特性又易于维护的代码。