Administrator
发布于 2026-09-03 / 8,662 阅读
0
0

鸿蒙设计模式综述:从单例到响应式架构的工程化选型

鸿蒙百篇系列第 77 篇 · 专家

第 50 篇讲过设计模式在鸿蒙里的基础应用。到第 76 篇为止,你已经看过分层架构、错误体系、性能治理、热修复、跨平台工程等专家级实践。这篇把它们串起来——站在架构层面,系统梳理 ArkTS/ArkUI 中设计模式的选型与落地。

设计模式不是背出来的,是被代码量逼出来的。当你的应用超过 30 个页面、模块间依赖错综复杂时,选对模式和选错模式的差距,就是"改一晚上能上线"和"改一星期还在填坑"的差距。

一、单例模式:AppStorage vs 手写单例

单例在鸿蒙里是最容易被滥用的模式。很多开发者从 Android 迁移过来,习惯写一个 getInstance() 的全局管理类。但 ArkUI 框架本身已经提供了单例基础设施。

框架内置的单例

内置单例

作用域

典型用途

线程安全

AppStorage

应用级(进程内)

全局 UI 状态共享

UI 线程内安全

LocalStorage

页面级(Ability 内)

页面间状态共享

UI 线程内安全

AppStorageV2

应用级(支持字段级追踪)

复杂对象全局状态

UI 线程内安全

PersistentStorage

应用级(持久化)

偏好持久化

UI 线程内安全

第 3 篇和第 58 篇讲过 AppStorage 的基本用法。这里要说的是:什么情况下你仍然需要手写单例。

什么时候需要手写单例

AppStorage 管的是 UI 状态。如果你的对象不参与 UI 渲染,只做业务逻辑(比如网络层、数据库管理器、日志管理器),用 AppStorage 就不合适——它会把数据绑定到 UI 框架上,引发不必要的渲染开销。

// 业务层单例:不依赖 UI 框架
export class NetworkManager {
  private static instance: NetworkManager | null = null
  private baseURL: string = ''
  private interceptors: RequestInterceptor[] = []

  private constructor() {}

  static getInstance(): NetworkManager {
    if (NetworkManager.instance === null) {
      NetworkManager.instance = new NetworkManager()
    }
    return NetworkManager.instance
  }

  setBaseURL(url: string): void {
    this.baseURL = url
  }

  addInterceptor(interceptor: RequestInterceptor): void {
    this.interceptors.push(interceptor)
  }

  async request(config: RequestConfig): Promise<Response> {
    // 拦截器链处理
    for (const interceptor of this.interceptors) {
      config = await interceptor.onRequest(config)
    }
    // 发起请求...
    return {} as Response
  }
}

选型决策

场景

推荐方案

理由

全局主题色、语言

AppStorage + @StorageProp

UI 需要响应变化

登录用户信息

AppStorageV2 + @Trace

对象字段级追踪,避免整体刷新

网络层、数据库管理器

手写单例

不参与 UI 渲染

跨页面共享临时状态

LocalStorage

生命周期随 Ability,自动清理

偏好持久化

PersistentStorage

框架自动持久化恢复

二、观察者模式:状态装饰器的本质

ArkUI 的 @State、@Link、@Watch 本质上就是观察者模式的框架级实现。理解这一点,才能在复杂场景下做出正确的状态管理选型。

V1 vs V2 状态管理

第 3 篇讲过 V1 的基本装饰器。API 12 之后,ArkUI 推出了状态管理 V2,核心差异在观察粒度:

// V1:代理观察,只能感知第一层
@Observed
class UserInfo {
  name: string = ''
  age: number = 0
  address: Address = new Address()  // 嵌套对象的属性变化无法感知
}

// V2:数据本身可观察,支持深度观测
@ObservedV2
class UserInfoV2 {
  @Trace name: string = ''
  @Trace age: number = 0
  @Trace address: AddressV2 = new AddressV2()
}

@ObservedV2
class AddressV2 {
  @Trace city: string = ''
  @Trace street: string = ''
}

// 只有 city 变化时,只更新引用 city 的 Text 组件
@ComponentV2
struct UserCard {
  @Param user: UserInfoV2 = new UserInfoV2()

  build() {
    Column() {
      Text(this.user.name)
      Text(this.user.address.city)  // 只在 city 变化时重渲染
    }
  }
}

特性

V1(@Observed/@ObjectLink)

V2(@ObservedV2/@Trace)

观察深度

仅第一层属性

任意深度

嵌套对象

需要逐层 @Observed

自动支持

数组元素更新

整体刷新

元素级精准更新

状态与 UI 耦合

状态变量绑定 UI

可独立于 UI 存在

性能

冗余更新较多

最小化更新

@Watch 的正确用法

@Watch 是观察者模式的轻量补充,但它不是万能的:

@Entry
@Component
struct OrderPage {
  @State @Watch('onPriceChange') price: number = 0
  @State @Watch('onQtyChange') quantity: number = 1
  @State total: number = 0

  onPriceChange() {
    // 注意:这里不要做重计算,只做派生状态更新
    this.total = this.price * this.quantity
  }

  onQtyChange() {
    this.total = this.price * this.quantity
  }

  build() {
    // ...
  }
}

@Watch 回调在主线程同步执行。如果你在里面做网络请求或文件 IO,会卡帧。它只适合做轻量的派生计算。

三、策略模式:条件渲染的工程化

策略模式在 ArkUI 中的应用场景比你想的多。最典型的是:同一个位置根据不同条件渲染完全不同的内容。

反模式:堆 if/else

// 坏例子:随着业务增长,这个 build 会越来越长
@Component
struct ContentArea {
  @State contentType: string = 'article'

  build() {
    Column() {
      if (this.contentType === 'article') {
        // 50 行文章渲染逻辑
      } else if (this.contentType === 'video') {
        // 50 行视频渲染逻辑
      } else if (this.contentType === 'live') {
        // 50 行直播渲染逻辑
      } else if (this.contentType === 'gallery') {
        // 50 行图集渲染逻辑
      }
    }
  }
}

策略模式:Builder 注册

// 定义渲染策略接口
interface ContentStrategy {
  build(content: ContentData): void
}

@Builder
function ArticleBuilder(content: ContentData) {
  Column() {
    Text(content.title).fontSize(20)
    Text(content.body).fontSize(16)
  }
}

@Builder
function VideoBuilder(content: ContentData) {
  Column() {
    Video({ src: content.videoUrl })
    Text(content.title)
  }
}

@Builder
function LiveBuilder(content: ContentData) {
  Column() {
    Text('直播中: ' + content.title)
    // 直播组件
  }
}

// 策略注册表
const contentStrategies: Record<string, (content: ContentData) => void> = {
  'article': ArticleBuilder,
  'video': VideoBuilder,
  'live': LiveBuilder
}

// 使用方:一行调度
@Component
struct ContentArea {
  @Prop contentType: string = 'article'
  @Prop contentData: ContentData = new ContentData()

  @Builder
  renderContent() {
    contentStrategies[this.contentType]?.(this.contentData)
  }

  build() {
    Column() {
      this.renderContent()
    }
  }
}

新增内容类型时,只需要注册一个新 Builder,不用改 ContentArea 组件。第 54 篇的表单引擎设计也用了类似的思路。

四、MVI:响应式架构在鸿蒙的落地

MVI(Model-View-Intent)是响应式架构在 UI 层的落地模式。和 MVC/MVP 相比,它的核心是单向数据流。

数据流

用户操作 → Intent → Model(状态变更) → View(UI 刷新)
    ↑                                          │
    └──────────── 事件驱动 ←───────────────────┘

代码骨架

// 定义状态
interface CounterState {
  count: number
  isLoading: boolean
  error: string | null
}

// 定义意图
type CounterIntent =
  | { action: 'increment' }
  | { action: 'decrement' }
  | { action: 'reset' }
  | { action: 'loadFromServer' }

// 状态容器
class CounterStore {
  @State state: CounterState = {
    count: 0,
    isLoading: false,
    error: null
  }

  dispatch(intent: CounterIntent): void {
    switch (intent.action) {
      case 'increment':
        this.state.count++
        break
      case 'decrement':
        this.state.count--
        break
      case 'reset':
        this.state.count = 0
        this.state.error = null
        break
      case 'loadFromServer':
        this.state.isLoading = true
        this.loadFromServer()
        break
    }
  }

  private async loadFromServer(): Promise<void> {
    try {
      const result = await fetchCount()
      this.state.count = result
      this.state.isLoading = false
    } catch (e) {
      this.state.error = (e as Error).message
      this.state.isLoading = false
    }
  }
}

// View 只负责渲染状态和派发意图
@Entry
@Component
struct CounterPage {
  private store: CounterStore = new CounterStore()

  build() {
    Column() {
      if (this.store.state.isLoading) {
        LoadingProgress()
      } else {
        Text(`Count: ${this.store.state.count}`)
      }

      if (this.store.state.error !== null) {
        Text(this.store.state.error).fontColor(Color.Red)
      }

      Button('+').onClick(() => this.store.dispatch({ action: 'increment' }))
      Button('-').onClick(() => this.store.dispatch({ action: 'decrement' }))
      Button('Reset').onClick(() => this.store.dispatch({ action: 'reset' }))
    }
  }
}

MVI vs MVC vs MVP

模式

数据流

状态管理

测试性

适用场景

MVC

双向

分散

差

简单页面

MVP

单向(Presenter 中转)

Presenter 管理

中

中等复杂度

MVI

严格单向

集中 Store

好

复杂状态、多数据源

第 39 篇讲过架构演进路线。MVI 是那条路线的终点——大型鸿蒙应用推荐用 MVI 管理核心业务流,简单页面用 @State + @Link 足够。

五、工厂模式:组件动态创建

第 34 篇讲组件化时提过工厂模式。这里补充一个实际场景:根据配置动态创建不同类型的卡片。

interface CardConfig {
  type: CardType
  data: CardData
}

enum CardType {
  BANNER = 0,
  LIST = 1,
  GRID = 2,
  VIDEO = 3
}

class CardFactory {
  @Builder
  static createCard(config: CardConfig): void {
    switch (config.type) {
      case CardType.BANNER:
        BannerCard({ data: config.data })
        break
      case CardType.LIST:
        ListCard({ data: config.data })
        break
      case CardType.GRID:
        GridCard({ data: config.data })
        break
      case CardType.VIDEO:
        VideoCard({ data: config.data })
        break
    }
  }
}

// 服务端下发卡片配置列表,端侧动态渲染
@Component
struct DynamicFeedPage {
  @State cardConfigs: CardConfig[] = []

  build() {
    List() {
      LazyForEach(this.dataSource, (config: CardConfig) => {
        ListItem() {
          CardFactory.createCard(config)
        }
      }, (config: CardConfig) => config.data.id)
    }
  }
}

这种模式在信息流、动态首页等场景很常见。服务端控制内容编排,端侧只管渲染。

六、适配器模式:NAPI 桥接

第 43 篇和第 64 篇讲过 NAPI 原生模块开发。适配器模式在这里的作用是:隔离 ArkTS 层和 C++ 层的数据结构差异。

// C++ 层返回的数据结构
interface NativeDeviceResult {
  deviceId: string
  rawName: Uint8Array  // 原始字节,需要解码
  status: number
  extras: Map<string, string>
}

// ArkTS 层使用的领域模型
class DeviceModel {
  id: string
  displayName: string
  isActive: boolean
  extraInfo: Record<string, string>

  constructor(id: string, name: string, active: boolean, extras: Record<string, string>) {
    this.id = id
    this.displayName = name
    this.isActive = active
    this.extraInfo = extras
  }
}

// 适配器:转换 + 解码
class DeviceAdapter {
  static toModel(raw: NativeDeviceResult): DeviceModel {
    return new DeviceModel(
      raw.deviceId,
      this.decodeUTF8(raw.rawName),
      raw.status === 1,
      this.mapToObject(raw.extras)
    )
  }

  private static decodeUTF8(bytes: Uint8Array): string {
    const decoder = new TextDecoder('utf-8')
    return decoder.decode(bytes)
  }

  private static mapToObject(map: Map<string, string>): Record<string, string> {
    const obj: Record<string, string> = {}
    map.forEach((value, key) => {
      obj[key] = value
    })
    return obj
  }
}

适配器的价值在于:当 C++ 层的数据结构变化时,只需要改 Adapter,ArkTS 业务层完全不受影响。

七、模式选型速查表

问题场景

推荐模式

鸿蒙落地方式

相关篇目

全局唯一实例

单例

AppStorage / 手写

第 3、58 篇

状态变化通知

观察者

@State/@Watch/AppStorageV2

第 3、77 篇

同一位置不同渲染

策略

Builder 注册表

第 54、77 篇

复杂状态管理

MVI

Store + dispatch

第 39、77 篇

动态组件创建

工厂

switch + Builder

第 34、77 篇

跨层接口转换

适配器

NAPI 包装层

第 43、64 篇

按序构建对象

建造者

链式 API 设计

第 48 篇

跨模块通信

发布订阅

EventEmitter / LocalStorage

第 71 篇

小结

设计模式在鸿蒙开发中的落地,和教科书上的理论有很大不同。框架已经内置了单例(AppStorage)、观察者(状态装饰器)、建造者(链式配置)等模式的实现,你的工作是在此基础上补齐策略、工厂、适配器、MVI 这些工程级模式。

选择模式的核心标准只有一个:它是否降低了你的代码复杂度。如果一个模式引入的抽象层比它解决的问题还复杂,那就是过度设计。


评论