HarmonyOS ArkTS 倒计时组件实战:设计模式反思篇 - 当AI建议用策略模式时,我选择了质疑
最近在重构一个 HarmonyOS 倒计时组件,遇到了一个复杂的条件分支。我让AI帮忙优化,它建议使用策略模式。但我质疑了这个建议,最终找到了更简洁实用的方案。想和大家分享一下这个过程,特别是在AI时代,如何保持独立思考,识别过度设计。
📋 背景:一个复杂的条件分支
项目中有一个 decideWhatToShow() 方法,需要根据多种条件决定显示的时间格式:
decideWhatToShow() {
/*倒计时分支较多,注意保留注释减小维护难度*/
// 不开启自定义时:hh:mm:ss 或 D天hh小时
// 开启自定义时:根据样式类型显示不同格式
if (this.remainingTime == 0) {
return
}
const above48h = (this.totalTime > 60 * 60 * 48) && this.allow48ToDay
let key = ''
if (!this.originalEnableCustomStyle) {
key = above48h ? 'D天hh小时' : 'hh:1mm:2ss'
} else {
this.customStyleEnabled = true
if (this.activeCustomStyleType === 0) {
key = 'hh:1mm:2ss'
this.shouldHideHour && (key = key.replace('hh:', ''))
this.shouldShowMS && (key = key + '.ms')
} else if (this.activeCustomStyleType === 1) {
key = 'D天hh小时'
} else if (this.activeCustomStyleType === 2) {
key = 'ss'
this.shouldShowMS && (key = key + '.ms')
} else if (this.activeCustomStyleType === 3) {
key = '自定义文本'
} else {
this.customStyleEnabled = false
if (above48h) {
key = 'D天hh小时'
} else {
key = 'hh:1mm:2ss'
this.shouldHideHour && (key = key.replace('hh:', ''))
this.shouldShowMS && (key = key + '.ms')
}
}
}
this.assignFlags(key)
}
这段代码的问题很明显:
- 60+ 行的 if-else 嵌套,看着就头疼
- 逻辑分散,维护起来很麻烦
- 有重复代码(格式构建逻辑出现多次)
- 连注释都在说"分支较多,注意保留注释减小维护难度"
看到这段代码,我的第一反应就是:这必须重构! 但我一时想不出特别好的方案,于是让AI帮忙看看。
🤖 AI的建议:策略模式
AI分析后,建议使用策略模式。它说这是处理多种算法/策略的经典设计模式,正好符合这个场景,并且给出了详细的实现方案:
AI的方案设计
AI设计了一个基于 Record 数据驱动的策略模式:
// 策略上下文
interface FormatStrategyContext {
remainingTime: number
totalTime: number
allow48ToDay: boolean
shouldHideHour: boolean
shouldShowMS: boolean
activeCustomStyleType?: number
originalEnableCustomStyle: boolean
}
// 策略结果
interface FormatStrategyResult {
key: string
customStyleEnabled: boolean
}
// 策略函数类型
type FormatStrategy = (context: FormatStrategyContext) => FormatStrategyResult
// 策略 Record
const formatStrategies: Record<string, FormatStrategy> = {
'default-above48h': (context) => ({
key: 'D天hh小时',
customStyleEnabled: false
}),
'default-below48h': (context) => ({
key: buildBaseTimeFormat(context.shouldHideHour, context.shouldShowMS),
customStyleEnabled: false
}),
'custom-type-0': (context) => ({
key: buildBaseTimeFormat(context.shouldHideHour, context.shouldShowMS),
customStyleEnabled: true
}),
// ... 8个策略函数
}
// 策略选择和执行
function selectStrategyKey(context: FormatStrategyContext): string { /* ... */ }
function executeFormatStrategy(context: FormatStrategyContext): FormatStrategyResult { /* ... */ }
// 重构后的主方法
decideWhatToShow() {
if (this.remainingTime == 0) return
const context: FormatStrategyContext = {
remainingTime: this.remainingTime,
totalTime: this.totalTime,
// ... 构建上下文
}
const result = executeFormatStrategy(context)
this.customStyleEnabled = result.customStyleEnabled
this.assignFlags(result.key)
}
AI的方案看起来确实很"优雅":
- ✅ 消除了复杂的 if-else 分支,主方法从 60+ 行减少到 20 行
- ✅ 使用了经典的设计模式
- ✅ 符合函数式风格,用 Record 存储函数
- ✅ 可扩展性强,新增策略只需在 Record 中添加
但当我仔细看代码统计时,发现了问题:
- 增加了约 160 行代码(类型定义 + 策略函数 + 选择逻辑)
- 主方法从 60 行减少到 20 行
- 总代码量从 60 行增加到 180 行
❓ 我的质疑:这是优化吗?
看到这个数据,我开始质疑AI的建议:
"增加了100多行代码,功能一样,性能差不多,这真的是优化吗?"
说实话,这种"为了优雅而增加代码"的做法,我在同事的代码中也经常看到。大家似乎都认为"用了设计模式 = 代码质量高",但真的是这样吗?
数据对比分析
先看看数据对比:
| 指标 | 原始实现 | AI的策略模式 | 变化 |
|---|---|---|---|
| 代码行数 | 60行 | 180行 | +120行 |
| 函数数量 | 1个 | 5个 | +4个 |
| 类型定义 | 0个 | 3个 | +3个 |
| 性能 | 基准 | 相同 | 无变化 |
| 功能 | 完整 | 完整 | 无变化 |
仔细分析一下:
- 代码量大幅增加:从 60 行增加到 180 行,增加了 200%
- 功能完全一样:没有任何新功能
- 性能没有提升:甚至可能因为函数调用有微小的开销
- 复杂度增加:需要理解策略模式、上下文、选择逻辑等概念
这真的是优化吗?还是为了"优雅"而过度设计?
我意识到,这种"为了用设计模式而用设计模式"的做法,在团队代码中太常见了。大家似乎都认为"用了设计模式 = 代码质量高",但忽略了代码简洁性的价值。
🤔 深入分析:AI的建议真的适合吗?
AI 用了“类/上下文/选择器”这套偏 OOP 的惯性写法,是否为当前简单场景付出了过高的复杂度成本?
我开始分析策略模式真正适用的场景:
- 策略数量多且会持续增长(>10个策略)
- 每个策略逻辑复杂(有大量独立逻辑)
- 需要频繁扩展新策略
- 策略需要独立测试
再看看当前场景:
- 策略数量固定:只有 8 种格式,且基本固定
- 策略逻辑简单:大部分只是返回不同的字符串
- 扩展需求不明确:未来是否会频繁扩展未知
- 测试需求一般:逻辑简单,测试成本低
AI的建议虽然"优雅",但把策略模式和面向对象实现强绑定:
- ❌ 违反了 YAGNI 原则(You Aren't Gonna Need It)
- ❌ 违反了 KISS 原则(Keep It Simple, Stupid)
- ❌ 这是过度设计(Over-engineering)
这让我想到,在团队代码评审中,我经常看到类似的过度设计。大家似乎都认为"用了设计模式 = 代码质量高",但忽略了实际场景和代码简洁性的重要性。
🔄 寻找更好的方案
认识到AI的策略模式建议是过度设计后,我开始寻找更合适的方案。AI又建议了一个轻量级的优化:
// 只提取重复的格式构建逻辑
function buildTimeFormatKey(shouldHideHour: boolean, shouldShowMS: boolean): string {
let key = 'hh:1mm:2ss'
if (shouldHideHour) {
key = key.replace('hh:', '')
}
if (shouldShowMS) {
key = key + '.ms'
}
return key
}
decideWhatToShow() {
// ... 原有逻辑,但使用 buildTimeFormatKey 消除重复
key = buildTimeFormatKey(this.shouldHideHour ?? false, this.shouldShowMS ?? false)
}
这个方案:
- ✅ 只增加 10 行代码
- ✅ 消除了重复逻辑
- ✅ 保持了原有结构
✨ 最终方案:场景映射
经过思考,我找到了一个更简洁的方案,用 Record 直接映射场景到格式:
decideWhatToShow() {
if (this.remainingTime == 0) {
return //到时保持最终样式
}
const above48h = (this.totalTime > 60 * 60 * 48) && this.allow48ToDay
this.customStyleEnabled = this.originalEnableCustomStyle &&
this.activeCustomStyleType != null && this.activeCustomStyleType !== -1
const occasionKey = this.originalEnableCustomStyle
? ('启用自定义' + (this.activeCustomStyleType ?? -1))
: '禁用自定义'
// 提取重复的分钟格式构建逻辑
const minuteForm = (this.shouldHideHour ? '1mm:2ss' : 'hh:1mm:2ss') + (this.shouldShowMS ? '.ms' : '')
const occasions: Record<string, string> = {
'禁用自定义': above48h ? 'D天hh小时' : 'hh:1mm:2ss',
'启用自定义0': minuteForm,
'启用自定义1': 'D天hh小时',
'启用自定义2': this.shouldShowMS ? 'ss.ms' : 'ss',
'启用自定义3': '自定义文本',
'启用自定义-1': above48h ? 'D天hh小时' : minuteForm,
}
this.assignFlags(occasions[occasionKey] ?? occasions['禁用自定义'])
}
怎么看待AI的设计模式
AI 的写法更贴近 GoF 式“上下文+策略对象+选择器”的传统策略模式;
我去掉了 OOP 仪式,但意图一致。
根据最初的GoF模式描述来狭义理解的话,不完全吻合最初描述就不算xx模式,需要给出新的命名,但是我们更应该关注本质的思想,因此大可使用广义的解释,认为我的方法同时具备:
- 策略模式(广义):不同 key/occasion 选择不同格式生成策略(数据式/函数式实现依然是策略模式)。
- 状态模式(类比):不同 occasionKey 也像不同状态触发不同输出。
- 工厂模式(轻量):根据条件“生产”格式字符串,属于轻量工厂。
- 表驱动编程:完全基于查找表完成分派与输出。
为什么这个方案更好?
1. 代码量对比
| 方案 | 代码行数 | 增加量 |
|---|---|---|
| 原始实现 | 60行 | 基准 |
| 策略模式 | 180行 | +120行 |
| 轻量级优化 | 70行 | +10行 |
| 场景映射 | 25行 | -35行 |
2. 设计思路/可读性/维护性对比
| 维度 | OOP 策略模式 | 场景映射 |
|---|---|---|
| 设计思路 | 上下文+策略选择器+执行器,函数/类层层分派 | 字符串场景键直接映射格式,条件内联在值里 |
| 可读性 | 需要理解上下文、选择、执行的调用链 | 一眼看到“场景 → 格式”映射 |
| 维护性 | 改格式需改策略函数,新增场景需加函数+选择逻辑 | 改格式直接改表值,新增场景加一行表项 |
💡 精简结论与经验
结论
- 当前场景不需要重型/OOP 策略模式,轻量 Record/表驱动策略即可。
- 模式要看意图,不必绑死在 GoF/OOP 形式。
方案对比
| 维度 | 原始 | OOP 策略 | 场景映射 |
|---|---|---|---|
| 代码行数 | 60 | 180 | 25 |
| 可读性 | 中 | 高 | 很高 |
| 可维护性 | 中 | 高 | 高 |
| 可扩展性 | 低 | 很高 | 高 |
| 复杂度 | 中 | 高 | 低 |
| 推荐度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
工程经验
- 简洁性优先:别为“优雅”牺牲体量和维护成本。
- 数据驱动要简单:映射表 > 过度抽象的函数分派。
- 识别过度设计:代码量增长>50%,功能/性能无收益,要警惕。
- 工程权衡:代码量 vs 维护性,简洁 vs 扩展,当前 vs 未来。
- AI 时代的独立思考:AI建议需经工程判断,不盲从。
实用检查表
- 这次“优化”代码量涨了多少?功能/性能有提升吗?
- 是否为了用设计模式而用设计模式?
- 映射表/数据驱动是否能更简单地解决?
- 场景是否真的需要重型架构?未来扩展是否明确?
参考资料: