【HarmonyOS 7新能力|047】ModularObjectExtensionAbility工程封装:把接入逻辑放进可维护的分层结构

【HarmonyOS 7新能力|047】ModularObjectExtensionAbility工程封装:把接入逻辑放进可维护的分层结构

跨应用调用最容易写成"拿到对象后直接调用",也最容易在版本升级、权限变化和异常恢复时失控。真正困难的并不是把一次请求发出去,而是明确能力由谁提供、调用方可以依赖什么、对象失效后如何恢复,以及失败是否允许重试。本文不把某个接口名称当成完整方案,而是围绕 ModularObjectExtensionAbility 所代表的模块化对象扩展思路,给出一套可落地的工程封装方法。示例采用 ArkTS 风格的领域接口,具体系统 API、权限名和配置项应以当前 SDK 与华为开发者文档为准。

一、先划清能力边界:跨应用调用不是普通函数调用

进程内函数共享同一份内存、类型和生命周期;跨应用能力调用则跨越了身份、进程、版本与资源边界。调用方看到的"对象"更适合被理解为一个受约束的能力入口,而不是可以永久持有的本地实例。只要把这一点想清楚,超时、死亡重连、权限拒绝和协议演进就会自然进入设计范围。

建议先把需求写成能力契约:输入是什么、输出是什么、是否产生副作用、是否可取消、失败后能否安全重试。不要让页面直接认识底层连接对象,也不要把系统返回码散落到业务代码中。

ts 复制代码
export interface DocumentPreviewRequest {
  requestId: string
  uri: string
  maxWidth: number
}

export interface DocumentPreviewResult {
  requestId: string
  thumbnailUri: string
  generatedAt: number
}

这份契约只表达业务事实,不携带页面组件、Context 或平台连接句柄,因此可以单测,也方便未来替换实现。

二、用四层结构隔离平台变化

推荐把工程拆成四层。第一层是页面和 ViewModel,只负责用户输入、加载状态与结果展示;第二层是能力网关,对外提供稳定方法;第三层是连接与扩展适配器,处理发现、绑定、调用和断开;第四层是安全、日志、缓存等基础设施。依赖方向只能向下,底层不得反向操作页面。

ts 复制代码
export interface PreviewGateway {
  createPreview(input: DocumentPreviewRequest): Promise<DocumentPreviewResult>
  cancel(requestId: string): Promise<void>
}

export class PreviewViewModel {
  constructor(private readonly gateway: PreviewGateway) {}

  async load(input: DocumentPreviewRequest): Promise<DocumentPreviewResult> {
    return this.gateway.createPreview(input)
  }
}

这种结构的价值不在于文件夹数量,而在于让业务代码只依赖 PreviewGateway。即使平台侧对象获取方式发生变化,页面与领域模型也无需跟着重写。

三、契约必须可演进,而不是一次性 DTO

跨应用两端未必同时升级。若新增字段就破坏旧调用方,模块化很快会变成版本锁死。请求中应保留明确的协议版本、可选能力和可忽略扩展;响应则应使用稳定错误码,不把底层异常字符串当成公开协议。

ts 复制代码
export interface CapabilityEnvelope<T> {
  protocolVersion: number
  capability: string
  traceId: string
  payload: T
  options?: Record<string, string>
}

export const PREVIEW_PROTOCOL = 1

协议升级遵循"新增优先、删除谨慎"。新提供方应能处理旧版本请求;旧提供方遇到无法理解的新版本时返回明确的"不支持版本",让调用方降级,而不是崩溃或静默产出错误数据。

四、把发现、连接与调用做成状态机

连接过程不是一个布尔值。真实状态至少包括空闲、发现中、连接中、可用、恢复中和已关闭。把它们压成 connected=true/false 会导致并发点击重复建连,也无法区分用户主动关闭与意外断开。

ts 复制代码
export type LinkState =
  | 'idle'
  | 'discovering'
  | 'connecting'
  | 'ready'
  | 'recovering'
  | 'closed'

export interface CapabilityLink {
  state(): LinkState
  connect(): Promise<void>
  close(): Promise<void>
}

网关应复用同一个进行中的连接 Promise,让多个并发请求等待同一轮建连。进入 closed 后不再隐式恢复,避免页面退出后后台又悄悄拉起能力。

五、身份与权限校验必须在提供端再次执行

调用方隐藏按钮只能改善体验,不能构成安全边界。提供端接到每一次请求时都要重新判断调用身份、能力范围、参数合法性和资源归属。尤其是 URI、文件路径、账号标识等字段,不能因为来自"已连接对象"就默认可信。

ts 复制代码
export interface CallPrincipal {
  callerId: string
  permissions: string[]
}

export class AccessPolicy {
  assertPreviewAllowed(principal: CallPrincipal, uri: string): void {
    if (!principal.permissions.includes('document.preview')) {
      throw new DomainError('PERMISSION_DENIED', '当前调用方无预览权限')
    }
    if (!uri.startsWith('file://') && !uri.startsWith('datashare://')) {
      throw new DomainError('INVALID_URI', '不支持的资源地址')
    }
  }
}

示例中的权限字符串只是领域表达,并非系统权限声明。工程中应把平台鉴权结果映射为领域权限,并确保清单、运行时授权、隐私说明和实际行为一致。

六、生命周期要绑定到业务会话

扩展能力可能被系统回收,调用方页面也可能进入后台或销毁。如果把远端对象放进全局单例长期持有,容易积累监听器、泄漏资源,甚至在错误账号下复用旧会话。更稳妥的做法是让连接由一个明确的业务会话拥有。

ts 复制代码
export class PreviewSession {
  private disposed = false

  constructor(private readonly link: CapabilityLink) {}

  async start(): Promise<void> {
    if (this.disposed) throw new DomainError('SESSION_CLOSED', '会话已结束')
    await this.link.connect()
  }

  async dispose(): Promise<void> {
    if (this.disposed) return
    this.disposed = true
    await this.link.close()
  }
}

页面退出、账号切换或业务流程结束时显式释放。意外断开只触发有限恢复,不应无限重连,更不能绕开用户可见的状态提示。

七、错误模型要让上层知道下一步怎么做

"调用失败"对用户没有帮助,对代码也无法决策。至少应区分不可用、权限拒绝、版本不兼容、参数错误、超时、提供端繁忙和内部错误。每类错误都应给出是否可重试、是否需要用户操作以及是否允许降级。

ts 复制代码
export type DomainErrorCode =
  | 'CAPABILITY_UNAVAILABLE'
  | 'PERMISSION_DENIED'
  | 'VERSION_UNSUPPORTED'
  | 'INVALID_ARGUMENT'
  | 'TIMEOUT'
  | 'BUSY'
  | 'INTERNAL'
  | 'SESSION_CLOSED'
  | 'INVALID_URI'

export class DomainError extends Error {
  constructor(
    readonly code: DomainErrorCode,
    message: string,
    readonly retryable: boolean = false
  ) { super(message) }
}

适配器负责把系统错误转换成领域错误;页面只根据领域错误决定展示"重试""去授权"还是本地降级,避免 UI 依赖不稳定的错误文案。

八、重试之前先解决幂等与超时

跨进程超时不代表提供端没有执行。调用方如果直接重发"创建、支付、删除"等有副作用动作,可能造成重复结果。每个变更请求都应携带稳定 requestId,提供端在合理时间窗内缓存已完成结果,重复请求返回同一结果。

ts 复制代码
export class RetryPolicy {
  async run<T>(action: () => Promise<T>, maxAttempts = 2): Promise<T> {
    let last: Error | undefined
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try { return await action() } catch (error) {
        last = error as Error
        const retryable = error instanceof DomainError && error.retryable
        if (!retryable || attempt === maxAttempts) throw error
      }
    }
    throw last ?? new Error('unknown error')
  }
}

只有明确标记为可重试的瞬态错误才能进入重试,而且次数有限、带退避。权限、参数和协议错误重试没有意义,应立即返回。

九、并发控制应放在能力网关而不是按钮上

按钮防抖不能阻止多个页面、后台任务或重复回调同时访问同一能力。网关需要明确并发策略:只读操作可以并行,写操作按资源串行,相同请求可以合并,已取消请求不得覆盖新结果。

ts 复制代码
export class RequestRegistry {
  private readonly running = new Map<string, Promise<unknown>>()

  coalesce<T>(key: string, factory: () => Promise<T>): Promise<T> {
    const existing = this.running.get(key)
    if (existing) return existing as Promise<T>
    const task = factory().finally(() => this.running.delete(key))
    this.running.set(key, task)
    return task
  }
}

请求合并适合相同资源的只读查询。涉及写入时应使用资源级队列并保留顺序,不能为了"提速"盲目合并不同意图。

十、可观测性要串起调用两端

线上问题经常表现为"偶尔没反应"。没有统一 traceId,就无法判断时间消耗在发现、连接、鉴权、执行还是回调。每次调用应记录阶段、耗时、结果码和协议版本,但不得记录令牌、原始文件内容等敏感数据。

ts 复制代码
export interface CallMetric {
  traceId: string
  capability: string
  stage: 'discover' | 'connect' | 'authorize' | 'execute' | 'respond'
  durationMs: number
  result: 'ok' | 'error' | 'cancelled'
  errorCode?: DomainErrorCode
}

调用方和提供方使用同一 traceId,各自记录本段耗时。聚合后可以看到成功率、P95 延迟、断连率和版本分布,也能为降级阈值提供证据。

十一、用假实现与故障注入覆盖边界

跨应用能力不应等到真机联调才测试。先为网关提供内存假实现,覆盖成功、超时、断开、权限拒绝和旧协议响应;再做平台适配层集成测试,最后验证真实设备上的生命周期与权限路径。

ts 复制代码
export class FakePreviewGateway implements PreviewGateway {
  constructor(private readonly mode: 'success' | 'timeout' | 'denied') {}

  async createPreview(input: DocumentPreviewRequest): Promise<DocumentPreviewResult> {
    if (this.mode === 'denied') {
      throw new DomainError('PERMISSION_DENIED', '测试拒绝')
    }
    if (this.mode === 'timeout') {
      throw new DomainError('TIMEOUT', '测试超时', true)
    }
    return { requestId: input.requestId, thumbnailUri: 'memory://preview', generatedAt: 1 }
  }

  async cancel(_requestId: string): Promise<void> {}
}

重点断言用户可见状态和资源释放:重复点击是否只执行一次、页面退出后是否取消、断开后旧结果是否被丢弃、恢复后是否继续使用正确账号。

十二、落地清单:从"能调用"升级为"可维护"

上线前逐项确认:能力契约与副作用已说明;协议版本可兼容;页面不持有平台对象;连接状态机可回收;提供端重新鉴权;输入经过校验;错误可行动;写请求具备幂等键;重试次数有限;并发策略明确;日志不含敏感数据;断连和升级路径已测试。

ts 复制代码
export const releaseChecklist = {
  contractVersioned: true,
  providerAuthorization: true,
  lifecycleBounded: true,
  idempotencyEnabled: true,
  retryBounded: true,
  traceConnected: true,
  sensitiveLogsRemoved: true,
  failurePathsTested: true
}

ModularObjectExtensionAbility 的工程价值,不只是让对象跨越应用边界,而是让能力以契约化、可治理的方式被复用。把发现、连接、鉴权、协议、生命周期和观测都收进网关与适配层后,业务页面才能保持简单,平台升级和异常恢复也不会扩散成全工程重构。真正可靠的跨应用能力,应该在成功路径顺畅的同时,也对拒绝、断开、超时与版本差异有清晰答案。

相关推荐
李游Leo2 小时前
HarmonyOS 7 ArkTS 并发实战:Sendable、共享模块与跨线程对象传递机制
harmonyos
骑着蜗牛撵大象3273 小时前
从跨平台Linux到鸿蒙PC:基于Qt C++的Flameshot截图工具源码级适配
harmonyos·鸿蒙·es
威哥爱编程5 小时前
HarmonyOS 7 空间音频实战:降噪、美化、变声、空间渲染的节点编排
华为·harmonyos·arkts
威哥爱编程5 小时前
HarmonyOS 7 视觉 AI 实战:系统级场景化控件,低门槛接入端侧视觉能力
华为·harmonyos·arkts
贾伟康5 小时前
【HarmonyOS 7新能力|046】智慧手势工程封装:把接入逻辑放进可维护的分层结构
人机交互·harmonyos·arkts·arkui·手势识别
贾伟康6 小时前
【HarmonyOS 7新能力|050】Core File Kit mmap工程封装:把接入逻辑放进可维护的分层结构
性能优化·harmonyos·arkts·文件系统·mmap
贾伟康6 小时前
【HarmonyOS 7新能力|048】Taihe IPC工程封装:把接入逻辑放进可维护的分层结构
harmonyos·arkts·ipc·分布式通信·taihe
贾伟康6 小时前
【HarmonyOS 7新能力|045】LazyLayoutAlgorithm工程封装:把接入逻辑放进可维护的分层结构
性能优化·harmonyos·arkts·arkui·懒加载
梦想不只是梦与想16 小时前
鸿蒙 云测试:上架测试流程
harmonyos·鸿蒙·云测试