要达到至善至美,先得对许多事情不理解!如果我们理解得太快,恐怕也就理解得不透。------《白痴》,陀思妥耶夫斯基
本文记录了笔者在真实业务场景里遇到的适配问题,本文在保留技术方案设计的基础上,隐去了部分实现细节。
1. 背景介绍与关键问题
1.1 业务背景
对于 Android APP 的介绍
- 这是一个用于配合智能手表使用的 Android APP,可以用来与智能手表进行绑定、配对,并且在 APP 上完成手表的各项设置。
- 该 APP 可以与多块手表建立绑定关系,绑定关系是账号维度的。但同一时间内,只能通过蓝牙连接一块智能手表。
对于手表的介绍
- 该手表属于智能手表,与 Android APP 通过蓝牙进行配对和通信。
- 手表存在更新换代(例如一代表、二代表、三代表),不同代手表之间特性有差异:形态(圆/方)、分辨率、功能集、数据协议等。
- 手表的"表盘"支持编辑,用户可以设置表盘背景图片,所显示的时间样式、位置、颜色、字体,以及手表上显示的小部件(日期、天气、心跳、步数等)。
下图:编辑项示意.
这里采用了 Apple Watch 举例,实际上业内智能手表设计与 Apple Watch 同质化十分严重。
下图:与之配合使用的 APP 编辑页面。
这也是本文要讨论的主要设计内容。
1.2 问题核心
在接手这个模块时,我遇到的最主要问题是,代码里存在大量的判断手表代数、功能集的if-else
逻辑,嵌套多层判断。在这样的"祖传代码"堆积下,任何 bugfix
或者 feature 开发
都是一种灾难。
并且,未来还会继续出现更多新上市的智能手表,其功能集、适配特性 会越来越复杂。作为开发者,需要在原有老页面中增加对新手表的兼容。在开发新特性对应的新页面时,也要考虑到老手表升级后,会支持该新特性,复杂程度是呈指数上升的。
因此,可以将问题归纳为,如何让同一编辑页在面对不同设备特征时,动态展示对应功能和布局。
1.3 关键矛盾
- 功能抽象统一性 vs 设备特性多样性。
- 未来可扩展性 vs 当前实现复杂度。
2. 抽象建模
2.1 主导方向选择
在明确了核心问题后,对此进行抽象建模。如果具备一定的设计模式知识和模式识别能力,可以初步判断这是一个 策略模式 的典型应用场景。在进行架构主导方向设计时,我面临两种选择:
- 以设备特征为驱动------这代手表支持哪些功能 (例如通过
DeviceCapability
决定启用哪些功能模块、使用哪种模板)。 - 以表盘功能为核心------这项功能用在哪代手表上 (例如每个功能模块自己声明支持哪些设备特征,然后由系统去匹配组合)。
经过思考,在以上两个设计方向中,我选择了后者,采用思路是 以表盘功能为核心,设备特征为约束,原因有以下几点:
- 演化方向清晰:功能是不断叠加的(老表功能少,新表更全),未来加新表主要是"增加支持",不需要改老逻辑。
- 模块边界自然 :每个表盘功能模块(如照片表盘)可以自带一份"设备适配策略"(
capability descriptor
),决定自己在不同设备上的表现,而不是让设备去主导所有功能分支。 - 维护成本更低:当设备数量多时,设备驱动式架构容易膨胀(每新增设备要知道所有功能的细节);而功能驱动式让变化局限在单个功能域中。
- 可测试性强:功能模块可以独立调试、在模拟环境下加载不同设备能力配置。
需要强调的是,由于产品形态的限制(一个 APP 适配多代手表),功能 x 设备
的指数复杂度只能减缓,无法彻底将其消除。
2.2 核心模块抽象
DeviceProfile
:描述手表能力集,设备维度。Feature
:描述功能模块,功能维度。EditorStrategy
:封装具体编辑逻辑与表现,例如"表盘编辑页"逻辑。FeatureRegistry
:统一管理所有 Feature。
下图描述了核心模块的概念以及相互关系,注意"特性"与"能力集"的概念区别。"特性"是每一代表都具备的属性,例如长宽尺寸、产品名等;"能力集"则在不同代手表有所区别,增加或减少能力都有可能,例如一代手表表只具备ABC能力,到了二代手表则具备BCDEF能力。

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 分层结构
- 设备层:
DeviceProfile
- 功能层:
Feature
+EditorStrategy
- 调度层:
FeatureRegistry
- 展示层:
EditorPage
+ViewModel
在分层结构中我省略了 Repository
层,这样做的原因是降低复杂度。如果存在多个页面使用相同的适配逻辑,则可以在 ViewModel
和 FeatureRegistry
、Strategy
之间增加 Repository
层,封装底层实现,供不同页面的 ViewModel
使用。
3.2 数据与调用流
使用 mermaid 绘制时序图如下:
3.3 UML 类关系图
-
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
分支判断和多层嵌套,每一次开发新功能都十分痛苦,并且耗费大量的开发、测试资源。此外,线上运行中的代码也存在很多"暗雷",不时会收到相应功能的用户投诉,苦不堪言。
可以说,对表盘编辑模块的重构已经刻不容缓 。然而,公司的产品发布战略不会因为这点小事而有所变更,源源不断地会有新手表、新功能需求给到研发侧。我们要做的,就是 为这样一辆高速行驶的汽车更换轮胎。这件事很有挑战,也很有意义。我有信心跟团队一起,在今年完成这个小小的"壮举",一万年太久,只争朝夕!
