【业务场景架构实战】7. 多代智能手表适配:Android APP 表盘编辑页的功能驱动设计

要达到至善至美,先得对许多事情不理解!如果我们理解得太快,恐怕也就理解得不透。------《白痴》,陀思妥耶夫斯基

本文记录了笔者在真实业务场景里遇到的适配问题,本文在保留技术方案设计的基础上,隐去了部分实现细节。

1. 背景介绍与关键问题

1.1 业务背景

对于 Android APP 的介绍

  1. 这是一个用于配合智能手表使用的 Android APP,可以用来与智能手表进行绑定、配对,并且在 APP 上完成手表的各项设置。
  2. 该 APP 可以与多块手表建立绑定关系,绑定关系是账号维度的。但同一时间内,只能通过蓝牙连接一块智能手表。

对于手表的介绍

  1. 该手表属于智能手表,与 Android APP 通过蓝牙进行配对和通信。
  2. 手表存在更新换代(例如一代表、二代表、三代表),不同代手表之间特性有差异:形态(圆/方)、分辨率、功能集、数据协议等。
  3. 手表的"表盘"支持编辑,用户可以设置表盘背景图片,所显示的时间样式、位置、颜色、字体,以及手表上显示的小部件(日期、天气、心跳、步数等)。

下图:编辑项示意.

这里采用了 Apple Watch 举例,实际上业内智能手表设计与 Apple Watch 同质化十分严重。
下图:与之配合使用的 APP 编辑页面。

这也是本文要讨论的主要设计内容。

1.2 问题核心

在接手这个模块时,我遇到的最主要问题是,代码里存在大量的判断手表代数、功能集的if-else逻辑,嵌套多层判断。在这样的"祖传代码"堆积下,任何 bugfix 或者 feature 开发 都是一种灾难。

并且,未来还会继续出现更多新上市的智能手表,其功能集、适配特性 会越来越复杂。作为开发者,需要在原有老页面中增加对新手表的兼容。在开发新特性对应的新页面时,也要考虑到老手表升级后,会支持该新特性,复杂程度是呈指数上升的。

因此,可以将问题归纳为,如何让同一编辑页在面对不同设备特征时,动态展示对应功能和布局

1.3 关键矛盾

  • 功能抽象统一性 vs 设备特性多样性
  • 未来可扩展性 vs 当前实现复杂度

2. 抽象建模

2.1 主导方向选择

在明确了核心问题后,对此进行抽象建模。如果具备一定的设计模式知识和模式识别能力,可以初步判断这是一个 策略模式 的典型应用场景。在进行架构主导方向设计时,我面临两种选择:

  • 以设备特征为驱动------这代手表支持哪些功能 (例如通过 DeviceCapability 决定启用哪些功能模块、使用哪种模板)。
  • 以表盘功能为核心------这项功能用在哪代手表上 (例如每个功能模块自己声明支持哪些设备特征,然后由系统去匹配组合)。

经过思考,在以上两个设计方向中,我选择了后者,采用思路是 以表盘功能为核心,设备特征为约束,原因有以下几点:

  1. 演化方向清晰:功能是不断叠加的(老表功能少,新表更全),未来加新表主要是"增加支持",不需要改老逻辑。
  2. 模块边界自然 :每个表盘功能模块(如照片表盘)可以自带一份"设备适配策略"(capability descriptor),决定自己在不同设备上的表现,而不是让设备去主导所有功能分支。
  3. 维护成本更低:当设备数量多时,设备驱动式架构容易膨胀(每新增设备要知道所有功能的细节);而功能驱动式让变化局限在单个功能域中。
  4. 可测试性强:功能模块可以独立调试、在模拟环境下加载不同设备能力配置。

需要强调的是,由于产品形态的限制(一个 APP 适配多代手表),功能 x 设备 的指数复杂度只能减缓,无法彻底将其消除。

2.2 核心模块抽象

  • DeviceProfile:描述手表能力集,设备维度。
  • Feature:描述功能模块,功能维度。
  • EditorStrategy:封装具体编辑逻辑与表现,例如"表盘编辑页"逻辑。
  • FeatureRegistry:统一管理所有 Feature。

下图描述了核心模块的概念以及相互关系,注意"特性"与"能力集"的概念区别。"特性"是每一代表都具备的属性,例如长宽尺寸、产品名等;"能力集"则在不同代手表有所区别,增加或减少能力都有可能,例如一代手表表只具备ABC能力,到了二代手表则具备BCDEF能力。

2.3 设计思想

  1. 功能为主导,设备为上下文。
  2. 每个功能模块自带设备适配策略。
  3. 遵循"开放-封闭"原则:新功能、新设备可独立扩展,无需修改现有模块。

这里面核心的内容是 功能为主导,设备为上下文 ,这句话的意思是:系统的设计中信放在"功能"这一抽象层,而不是具体的设备型号;但设备仍然作为功能运行的"语境"和"约束条件"。 拆开来看------

  • 功能为主导 :模块架构首先定义的是"表盘编辑"这一通用功能,它描述了用户想完成的事(选图、裁剪、同步等)。这些逻辑不应该被具体设备特性绑死,而应当是跨设备一致的。换句话说,系统从"功能模块"出发组织代码和逻辑。
  • 设备为上下文 :不同手表提供的能力、分辨率、形态,会影响功能的具体表现形式。设备并不是功能的"主人",而是功能运行的"环境"。功能在执行时,会读取设备上下文(通过 DeviceProfile)来自我调整策略。

总结来说就是:功能决定系统的骨架,设备决定它如何落地。

2.4 数据模型示例

kotlin 复制代码
// 设备
data class DeviceProfile(
    val id: String,
    val model: String,
    val shape: WatchShape, // ROUND or SQUARE
    val resolution: Size,
    val supportedFeatures: Set<FeatureType>,
    val extraCapabilities: Map<String, Any> = emptyMap() // Key-Value存储的属性集合
)

// 特性
interface Feature {
    val type: FeatureType
    fun createEditorStrategy(profile: DeviceProfile): EditorStrategy
}

// 特性策略
interface EditorStrategy {
    fun buildUiConfig(): UiConfig // UI 显示特性
    fun buildSyncPayload(data: EditedData): SyncPayload // 用来在 APP-WATCH 之间传输的配置,可格式化为 json
}

// 示例-策略的具体实现-原表的表盘编辑
class RoundPhotoDialStrategy(private val profile: DeviceProfile) : EditorStrategy {
    override fun buildUiConfig(): UiConfig = UiConfig(
        aspectRatio = 1f,
        editableAreas = listOf(/* 定义圆形表盘可编辑区域 */)
    )

    override fun buildSyncPayload(data: EditedData): SyncPayload {
        return SyncPayload(format = "v1", content = mapOf(/* 转换数据为设备协议 */))
    }
}

3. 分层设计

3.1 分层结构

  1. 设备层:DeviceProfile
  2. 功能层:Feature + EditorStrategy
  3. 调度层:FeatureRegistry
  4. 展示层:EditorPage + ViewModel

在分层结构中我省略了 Repository 层,这样做的原因是降低复杂度。如果存在多个页面使用相同的适配逻辑,则可以在 ViewModelFeatureRegistryStrategy 之间增加 Repository 层,封装底层实现,供不同页面的 ViewModel 使用。

3.2 数据与调用流

使用 mermaid 绘制时序图如下:

sequenceDiagram title 照片表盘编辑页调用链流程 actor User participant PhotoDialEditorPage participant PhotoDialVM participant FeatureRegistry participant PhotoDialFeature participant DeviceProfile participant EditorStrategy participant WatchDevice User ->> PhotoDialEditorPage: 打开编辑页 PhotoDialEditorPage ->> PhotoDialVM: 初始化并绑定 ViewModel PhotoDialVM ->> FeatureRegistry: 获取 PhotoDialFeature FeatureRegistry ->> PhotoDialFeature: 创建 Feature 实例 PhotoDialFeature ->> DeviceProfile: 读取设备能力 PhotoDialFeature ->> EditorStrategy: 根据 Profile 生成对应策略 EditorStrategy -->> PhotoDialVM: 返回策略实例 PhotoDialVM ->> EditorStrategy: buildUiConfig() EditorStrategy -->> PhotoDialVM: 返回 UI 配置 PhotoDialVM ->> PhotoDialEditorPage: 更新页面显示 User ->> PhotoDialEditorPage: 编辑完成并点击"确认" PhotoDialEditorPage ->> PhotoDialVM: saveEditedData(data) PhotoDialVM ->> EditorStrategy: buildSyncPayload(data) EditorStrategy -->> PhotoDialVM: 返回同步数据 PhotoDialVM ->> DeviceProfile: 根据设备上下文封装同步指令 PhotoDialVM ->> WatchDevice: 下发同步请求

3.3 UML 类关系图

classDiagram title 照片表盘编辑页架构类图 class PhotoDialEditorPage { +PhotoDialVM viewModel +render(uiConfig) +onConfirmClick() } class PhotoDialVM { +DeviceProfile deviceProfile +PhotoDialFeature feature +EditorStrategy strategy +loadUiConfig() +saveEditedData(data) } class FeatureRegistry { +getFeature(type): Feature } class PhotoDialFeature { +createEditorStrategy(profile: DeviceProfile): EditorStrategy } class EditorStrategy { <> +buildUiConfig(): UiConfig +buildSyncPayload(data): Payload } class RoundPhotoDialStrategy { +buildUiConfig() +buildSyncPayload(data) } class SquarePhotoDialStrategy { +buildUiConfig() +buildSyncPayload(data) } class DeviceProfile { +shape: ShapeType +resolution: Size +supportedFeatures: List } %% 关系定义 PhotoDialEditorPage --> PhotoDialVM : 持有 PhotoDialVM --> FeatureRegistry : 调用 FeatureRegistry --> PhotoDialFeature : 获取 PhotoDialFeature --> EditorStrategy : 创建策略 EditorStrategy <|.. RoundPhotoDialStrategy : 实现 EditorStrategy <|.. SquarePhotoDialStrategy : 实现 PhotoDialVM --> DeviceProfile : 读取能力 PhotoDialVM --> EditorStrategy : 调用 EditorStrategy --> DeviceProfile : 依赖上下文
  • EditorStrategy

    • 策略层定义了表盘编辑的差异化逻辑。
    • 不同设备形态通过不同策略实现。
  • DeviceProfile

    • 设备上下文提供形态、分辨率、功能支持等能力信息。
    • 驱动策略选择,而非主导流程。

3.4 UI 层解耦设计

  • 页面 PhotoDialEditorPage 仅负责渲染。
  • PhotoDialVM 负责根据设备和功能选择策略。
  • UiConfig 描述布局和可编辑区域,可 JSON 化,方便未来动态加载。

UI 层渲染示例

kotlin 复制代码
val strategy = feature.createEditorStrategy(profile)
val uiConfig = strategy.buildUiConfig()
photoDialEditorPage.render(uiConfig)

4. 实现细节

4.1 策略选择逻辑

  • 使用 工厂或注册表模式 生成对应策略。
  • 避免在代码中大量的 if-else 判断。
  • 新增设备类型时,只需要增加对应的策略类 即可。

4.2 扩展方案

  • 新增设备:添加新的 DeviceProfile,实现新的策略类。
  • 新增功能:实现新的 Feature + 对应策略
  • 未来脚本化:可使用 JSON 配置 UI 模板,动态加载适配逻辑。

4.3 测试与维护

  • 使用 Mock DeviceProfile 做单元测试,覆盖不同形态和分辨率。

5. 进一步思考

5.1 解耦的边界在哪里

解决这个业务场景问题的关键,是 把稳定的部分与可变的部分进行隔离。从而当系统发生演变时,避免代码改动发生蔓延。

5.2 策略模式的边际成本

在本文的方案里,将"功能特性"作为主导,"设备特征"作为上下文。对于这两个维度的变量,在它们同时发生变化时,会产生指数量级的适配工作

举例来看:此项目上,通常的节奏是,发布一款新手表时,会增加部分新功能,同时要求将这些新功能适配给市面上现存的设备版本。这就导致了两方面的开发工作:

  • 将老手表的功能对新手表进行适配。
  • 把新增加的功能移植给老手表。

因此,即使采用策略模式进行隔离,随着上市的手表不断增加,我们仍然面对着海量的适配成本。未来当业务增长到足够庞大的程度时,也许我会再写一篇文章,思考如何进行下一步设计。

5.3 从"设备差异"到"生态抽象"

当前的方案是基于"型号差异"进行抽象,随着生态扩张(手环、耳机、体脂秤),如果要在同一个 APP 中进行支持,要考虑把方案升级成通用的"智能设备模型"。到那时,APP 的定位应该是 可以自由组合的"功能容器",而非"单设备管控端"

6. 结语

在笔者加入到这个项目团队时,"手表管理"模块已经持续迭代了2年时间,先后由10余名同事进行过维护,在设备维度上,已经支持公司在 2年期间发布的共五代不同手表 。然而由于缺乏高视角的一致性设计和持续重构,其 架构腐化非常严重 ,模块内部存在大量的 if-else 分支判断和多层嵌套,每一次开发新功能都十分痛苦,并且耗费大量的开发、测试资源。此外,线上运行中的代码也存在很多"暗雷",不时会收到相应功能的用户投诉,苦不堪言。

可以说,对表盘编辑模块的重构已经刻不容缓 。然而,公司的产品发布战略不会因为这点小事而有所变更,源源不断地会有新手表、新功能需求给到研发侧。我们要做的,就是 为这样一辆高速行驶的汽车更换轮胎。这件事很有挑战,也很有意义。我有信心跟团队一起,在今年完成这个小小的"壮举",一万年太久,只争朝夕!

相关推荐
Jolie_Liang3 小时前
保险业多模态数据融合与智能化运营架构:技术演进、应用实践与发展趋势
大数据·人工智能·架构
aklry4 小时前
elpis之动态组件机制
javascript·vue.js·架构
澄澈i4 小时前
设计模式学习[20]---桥接模式
c++·学习·设计模式·桥接模式
brzhang4 小时前
高通把Arduino买了,你的“小破板”要变“AI核弹”了?
前端·后端·架构
我星期八休息5 小时前
C++异常处理全面解析:从基础到应用
java·开发语言·c++·人工智能·python·架构
new_daimond5 小时前
微服务网关技术详细介绍
微服务·云原生·架构
Light605 小时前
领码方案|微服务与SOA的世纪对话(4):迁移与避坑——从 SOA 到微服务的演进路线图
微服务·云原生·架构·自动化运维·容器化·服务治理·渐进式迁移
XYiFfang5 小时前
【Docker】解决Docker中“exec format error”错误:架构不匹配的完整指南
docker·容器·架构
失散135 小时前
分布式专题——35 Netty的使用和常用组件辨析
java·分布式·架构·netty