ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

设计模式实战:用观察者、策略、命令模式构建可扩展JavaScript计数器

2026/8/29 10:31:06 拓冰建站 浏览量
设计模式实战:用观察者、策略、命令模式构建可扩展JavaScript计数器 1. 项目概述从计数器看设计模式的实战价值最近在重温《设计模式》这本书发现很多朋友包括当年的我自己都有一个共同的困惑书里的例子比如那个经典的“鸭子模拟器”虽然道理都懂但总觉得离自己手头的业务代码有点远。抽象的概念记住了一到实战就不知道怎么用。今天我们不谈鸭子我们来聊一个更接地气、几乎每个前端开发者都写过的东西——计数器。没错就是一个简单的、能加能减能重置的数字计数器。你可能觉得这太简单了几行代码就能搞定跟“设计模式”这种听起来高大上的东西有什么关系这正是我想分享的核心设计模式的价值恰恰在于用看似“过度设计”的方式去解决简单代码在演化过程中必然会遇到的“复杂问题”。一个孤立的计数器按钮确实不需要模式。但当这个计数器需要被多个视图同步显示、需要记录历史操作、需要根据不同的业务规则进行不同的计数行为、或者需要轻松切换数据存储方式时原始的写法就会迅速变得难以维护。我们这次的目标就是用一个纯粹的JavaScript环境不依赖任何框架React, Vue, Angular从零开始用面向对象的思想重构这个计数器。我们会看到如何通过应用几个经典的设计模式让这个简单的功能模块变得职责清晰、易于扩展、高度解耦。无论你是想巩固面向对象基础还是想真正理解设计模式如何落地这个例子都是一个绝佳的切入点。你会发现模式不是枷锁而是让代码在需求变化面前保持优雅和韧性的工具箱。2. 核心思路为什么计数器需要设计模式在动手写代码之前我们必须先想清楚为什么要这么做。直接写一个全局变量let count 0然后绑定三个按钮的onclick事件分别进行countcount--count 0 最后更新一个span标签的innerText。这可能是99%的初学者会写出的版本功能完全正确。但让我们设想几个非常真实的业务场景多视图同步页面上不止一个地方显示这个计数值比如顶部导航栏有个徽章侧边栏有个统计面板主内容区有个大数字。每次操作后你需要手动找到所有显示元素并更新它们。漏掉一个就是Bug。操作历史与撤销产品经理说需要“撤销”功能用户可以回退到之前的计数状态。你的count变量只保存当前值历史数据丢了。业务规则复杂化计数器不再是简单的加减。比如VIP用户每次加10普通用户加1或者计数达到100时自动触发一个抽奖活动又或者需要支持“双击快速加5”等复合操作。状态持久化刷新页面后计数不能丢失。你需要把count存到localStorage、IndexedDB或者发送到后端服务器。数据存储的逻辑和业务逻辑搅在一起。单元测试你想测试“加”这个操作是否正确地更新了状态和视图。但视图操作DOM更新和状态变更紧密耦合很难进行隔离测试。面对这些场景最初的“面条代码”会迅速演变成一堆难以阅读和维护的if...else和散落在各处的document.querySelector。其根本问题在于缺乏抽象和职责混乱。状态管理、视图渲染、用户交互、业务规则、数据持久化全部揉成一团。设计模式提供了一套经过验证的抽象方法。对于计数器我们可以将其核心抽象为一个状态模型Model它只关心数字本身以及改变数字的规则。而视图View负责展示这个数字。两者之间通过一种通知机制Observer Pattern来同步而不是直接互相调用。这样无论增加多少种视图模型都无需修改无论模型的数据存储方式如何变化从内存到本地存储视图也无需关心。这就是我们重构的核心理念基于观察者模式的模型-视图分离。3. 模式选型与架构设计基于上述思路我们为这个计数器项目选择并组合使用三个经典的设计模式它们将共同构成一个清晰、稳固的架构。3.1 观察者模式实现模型与视图的解耦这是整个架构的“脊柱”。观察者模式定义了一种一对多的依赖关系当一个对象主题Subject的状态发生改变时所有依赖于它的对象观察者Observers都会得到通知并自动更新。在我们的计数器里主题Subject就是我们的计数器模型CounterModel。它持有状态计数值。观察者Observer就是各个需要显示计数值的视图组件CounterView可能是一个数字显示框一个进度条或者一个图表。CounterModel不需要知道具体有哪些CounterView它只需要维护一个观察者列表并在自己的状态count改变时调用一个通用的notify方法遍历列表告诉每个观察者“我变了这是新值”。每个CounterView在接收到通知后自己决定如何用这个新值去更新DOM。这样做的好处是巨大的我们可以动态地添加或移除视图比如在某个条件下隐藏统计面板而模型代码纹丝不动。视图和模型的修改可以独立进行符合开放-封闭原则。3.2 策略模式封装可互换的计数算法虽然基础计数器只有加、减、重置但我们可以预见未来可能会有不同的计数策略。比如一个“安全计数器”在减到负数时抛出警告一个“循环计数器”在达到最大值后归零。如果我们用if (mode ‘safe’) {...} else if (mode ‘circular’) {...}写在模型内部代码会变得臃肿且难以增加新策略。策略模式定义了一系列算法并将每个算法封装起来使它们可以相互替换。策略模式让算法的变化独立于使用它的客户端。我们将创建一个CountStrategy接口在JS中通常用一个函数或一个包含特定方法的对象来模拟然后实现BasicCountStrategy、SafeCountStrategy等具体策略。CounterModel并不直接实现加减逻辑而是持有一个CountStrategy的引用。当需要执行操作时它把当前值和操作类型‘increment‘ ’decrement‘委托给当前策略对象去计算。这样要增加一种新的计数规则我们只需要新增一个策略类然后在需要的时候注入到模型里即可完全不用修改模型和视图的既有代码。3.3 命令模式实现操作历史与撤销撤销/重做是一个经典需求。要实现它我们需要将每次操作如“加一”封装成一个对象。命令模式就是将“请求”封装成对象从而允许你用不同的请求对客户进行参数化支持请求的排队、记录日志以及可撤销的操作。我们将创建一个Command接口它可能有execute()和undo()方法。具体的IncrementCommand、DecrementCommand就封装了如何执行加一、以及如何撤销这个加一即减一。CounterModel不再直接修改count而是接收一个Command对象并调用其execute()方法。同时模型可以维护一个历史栈commandHistory每次执行命令后将其入栈。当用户点击“撤销”时就从栈顶取出命令调用其undo()方法然后再出栈。这不仅实现了撤销功能还带来了额外好处所有对模型状态的修改现在都有了一个明确的、可记录的“意图”对象。这对于调试、日志记录、甚至实现宏命令一键执行多个操作都提供了可能。架构总览最终我们的计数器将由以下核心类构成CounterModel继承自Subject持有count状态和当前CountStrategy接收并执行Command维护命令历史并在状态变更时通知所有视图。CounterView实现Observer接口在收到通知后更新特定的DOM元素。BasicCountStrategy/SafeCountStrategy实现具体的计数算法。IncrementCommand/DecrementCommand/ResetCommand封装具体的操作和撤销逻辑。一个顶层的App或初始化脚本负责组装这些对象创建模型、创建视图并将视图注册到模型、创建按钮并将点击事件绑定到创建和执行相应的命令。这个架构看起来比直接写count复杂得多但它为应对所有前述的复杂场景打下了坚实的基础并且每个类的职责都非常单一易于理解和测试。4. 核心实现从零构建模式化的计数器理论说得再多不如一行代码。我们现在就抛开任何框架用纯ES6的JavaScript来实现上面设计的架构。我会先给出关键代码片段并解释其背后的意图。4.1 实现观察者模式基础类首先我们需要实现观察者模式的通用部分。这通常包含两个角色Subject主题/被观察者和Observer观察者。在JavaScript中我们可以用类来模拟。// 观察者接口在JS中通常约定一个特定方法如 update class Observer { update(data) { throw new Error(子类必须实现 update 方法); } } // 主题基类 class Subject { constructor() { this.observers new Set(); // 使用Set避免重复注册 } // 注册观察者 attach(observer) { if (!(observer instanceof Observer)) { throw new TypeError(观察者必须实现 Observer 接口); } this.observers.add(observer); console.log(观察者已注册当前总数${this.observers.size}); } // 移除观察者 detach(observer) { const deleted this.observers.delete(observer); if (deleted) { console.log(观察者已移除当前总数${this.observers.size}); } } // 通知所有观察者 notify(data) { console.log(主题状态变更正在通知 ${this.observers.size} 个观察者...); for (const observer of this.observers) { // 异步通知避免某个观察者的错误阻塞其他观察者 Promise.resolve().then(() { try { observer.update(data); } catch (error) { console.error(通知观察者时发生错误:, error, observer); } }); } } }实现要点这里用Set存储观察者保证了唯一性。attach和detach方法提供了动态管理观察者的能力。notify方法采用异步通知Promise.resolve().then这是一个重要的实践技巧。它确保了即使某个视图更新时抛出异常比如DOM操作错误也不会影响其他视图的更新流程提高了系统的健壮性。同时将通知逻辑包裹在try...catch中便于错误定位。在Observer基类中抛出错误强制子类实现update方法这是一种接口的模拟。4.2 实现计数器模型CounterModel是我们的核心它继承自Subject管理状态并处理命令。class CounterModel extends Subject { constructor(initialCount 0, strategy new BasicCountStrategy()) { super(); // 调用父类Subject的构造函数 this._count initialCount; this._strategy strategy; this._history []; // 命令历史栈 this._historyIndex -1; // 当前历史指针 console.log(计数器模型初始化初始值${this._count}); } get count() { return this._count; } set count(value) { if (this._count ! value) { this._count value; // 状态改变通知所有观察者 this.notify({ count: this._count }); } } get strategy() { return this._strategy; } set strategy(newStrategy) { if (this._strategy ! newStrategy) { this._strategy newStrategy; console.log(计数策略已切换为:, newStrategy.constructor.name); // 策略切换可能影响当前值的显示逻辑比如安全策略下负数无效可以选择性地通知一次 // this.notify({ count: this._count }); } } // 执行命令 executeCommand(command) { // 执行新命令前清空“历史指针”之后的历史即重做分支 if (this._historyIndex this._history.length - 1) { this._history.splice(this._historyIndex 1); } command.execute(this); // 命令执行会修改 this.count this._history.push(command); this._historyIndex; console.log(命令执行成功历史记录数${this._history.length}, 当前指针${this._historyIndex}); } // 撤销 undo() { if (this._historyIndex 0) { const command this._history[this._historyIndex]; command.undo(this); // 命令撤销 this._historyIndex--; console.log(撤销成功当前指针${this._historyIndex}); } else { console.warn(没有更多历史可以撤销); } } // 重做 redo() { if (this._historyIndex this._history.length - 1) { this._historyIndex; const command this._history[this._historyIndex]; command.execute(this); // 重新执行命令 console.log(重做成功当前指针${this._historyIndex}); } else { console.warn(没有更多历史可以重做); } } // 直接操作不经过命令历史用于初始化或特定场景 setCountDirectly(newCount) { this.count newCount; } }关键设计解析状态管理count被设置为getter/setter。在setter中我们比较新旧值只有真正发生变化时才调用this.notify()。这避免了不必要的视图渲染是性能优化的小细节。命令历史_history数组和_historyIndex指针共同实现了经典的“撤销栈”。executeCommand方法在添加新命令时会清空当前指针之后的历史这是大多数编辑器的标准行为执行新操作后重做分支被丢弃。策略注入通过strategy的setter我们可以在运行时动态切换计数策略模型内部的其他代码完全不受影响。4.3 实现策略模式策略是独立的算法对象。我们先定义策略接口约定然后实现几个具体策略。// 策略接口约定所有策略类必须实现 calculate 方法 class CountStrategy { calculate(currentValue, operation, operand 1) { throw new Error(子类必须实现 calculate 方法); } } // 基础策略简单的加减 class BasicCountStrategy extends CountStrategy { calculate(currentValue, operation, operand 1) { switch (operation) { case increment: return currentValue operand; case decrement: return currentValue - operand; case reset: return 0; default: throw new Error(不支持的运算类型: ${operation}); } } } // 安全策略禁止结果为负数 class SafeCountStrategy extends CountStrategy { calculate(currentValue, operation, operand 1) { let newValue; switch (operation) { case increment: newValue currentValue operand; break; case decrement: newValue currentValue - operand; // 核心检查结果如果为负则返回当前值或抛出错误 if (newValue 0) { console.warn(安全策略计数结果不能为负数操作被阻止。); return currentValue; // 阻止操作返回原值 } break; case reset: newValue 0; break; default: throw new Error(不支持的运算类型: ${operation}); } return newValue; } } // 循环策略在指定范围内循环例如 0-9 class CircularCountStrategy extends CountStrategy { constructor(min 0, max 9) { super(); this.min min; this.max max; } calculate(currentValue, operation, operand 1) { let newValue; switch (operation) { case increment: newValue currentValue operand; if (newValue this.max) { newValue this.min; // 超过最大值回到最小值 } break; case decrement: newValue currentValue - operand; if (newValue this.min) { newValue this.max; // 小于最小值回到最大值 } break; case reset: newValue this.min; break; default: throw new Error(不支持的运算类型: ${operation}); } return newValue; } }策略模式的灵活性可以看到增加一个新的计数规则比如“每次乘2”我们只需要新建一个DoubleCountStrategy类即可。CounterModel的代码一行都不用改。这就是“对扩展开放对修改关闭”。4.4 实现命令模式命令对象封装了操作细节和撤销逻辑。// 命令接口 class Command { execute(model) { throw new Error(子类必须实现 execute 方法); } undo(model) { throw new Error(子类必须实现 undo 方法); } } // 具体的“增加”命令 class IncrementCommand extends Command { constructor(operand 1) { super(); this.operand operand; this.previousCount null; // 用于撤销 } execute(model) { // 执行前保存旧值用于撤销 this.previousCount model.count; const newCount model.strategy.calculate(model.count, increment, this.operand); model.setCountDirectly(newCount); // 使用直接设置避免触发命令历史循环 } undo(model) { if (this.previousCount ! null) { model.setCountDirectly(this.previousCount); } } } // 具体的“减少”命令 class DecrementCommand extends Command { constructor(operand 1) { super(); this.operand operand; this.previousCount null; } execute(model) { this.previousCount model.count; const newCount model.strategy.calculate(model.count, decrement, this.operand); model.setCountDirectly(newCount); } undo(model) { if (this.previousCount ! null) { model.setCountDirectly(this.previousCount); } } } // 重置命令 class ResetCommand extends Command { constructor() { super(); this.previousCount null; } execute(model) { this.previousCount model.count; const newCount model.strategy.calculate(model.count, reset); model.setCountDirectly(newCount); } undo(model) { if (this.previousCount ! null) { model.setCountDirectly(this.previousCount); } } }命令模式的关键每个命令对象都是一个完整的“操作快照”。execute方法知道如何应用操作undo方法知道如何回退。previousCount属性保存了执行前的状态这是实现撤销的一种简单方式。对于更复杂的操作命令对象可能需要保存更多的上下文信息。4.5 实现视图视图是观察者它订阅模型的变化。class CounterView extends Observer { constructor(elementId, label ) { super(); this.element document.getElementById(elementId); if (!this.element) { throw new Error(未找到ID为 ${elementId} 的DOM元素); } this.label label; this.update({ count: 0 }); // 初始化显示 console.log(计数器视图已创建绑定到元素: #${elementId}); } update(data) { // 收到模型通知更新DOM const displayText this.label ? ${this.label}: ${data.count} : data.count; // 使用textContent比innerHTML更安全、性能更好 this.element.textContent displayText; // 可以在这里根据数值添加一些样式变化比如负数变红 if (data.count 0) { this.element.style.color red; } else { this.element.style.color ; // 恢复默认 } console.log(视图更新: ${this.element.id} - ${displayText}); } }视图的职责单一CounterView只做一件事——当update被调用时用新数据更新自己绑定的DOM元素。它不知道模型内部如何计算也不知道还有其他什么视图。这种隔离使得视图极易测试和复用。4.6 应用组装与启动最后我们需要一个“粘合剂”把所有这些部分组装起来并连接到真实的HTML页面。假设我们有如下HTML结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 title设计模式实战计数器/title style body { font-family: sans-serif; margin: 2em; } .counter { margin: 1em 0; padding: 1em; border: 1px solid #ccc; } #display, #display2 { font-size: 2em; font-weight: bold; margin: 0.5em; } button { margin: 0.2em; padding: 0.5em 1em; } .history-btn { background-color: #f0f0f0; } .strategy-select { margin: 1em 0; } /style /head body h1设计模式实战可扩展计数器/h1 div classstrategy-select label选择计数策略/label select idstrategySelector option valuebasic基础策略/option option valuesafe安全策略禁止负数/option option valuecircular循环策略 (0-9)/option /select /div div classcounter p主显示区/p div iddisplay0/div button idbtnIncrement加一 (1)/button button idbtnDecrement减一 (-1)/button button idbtnReset重置 (0)/button button idbtnIncrement5加五 (5)/button /div div classcounter p副显示区同步/p div iddisplay20/div /div div classcounter p操作历史/p button idbtnUndo classhistory-btn撤销 (Undo)/button button idbtnRedo classhistory-btn重做 (Redo)/button button idbtnClearHistory classhistory-btn清空历史/button /div script srccounter-patterns.js/script !-- 上面所有的JS代码放在这个文件 -- script srcapp.js/script !-- 下面的组装代码放在这个文件 -- /body /html然后在app.js中我们进行组装// app.js - 应用组装与启动 document.addEventListener(DOMContentLoaded, function() { console.log(应用启动...); // 1. 创建模型实例使用基础策略 const counterModel new CounterModel(0, new BasicCountStrategy()); // 2. 创建多个视图实例并注册到模型 const mainDisplay new CounterView(display, 当前计数); const secondaryDisplay new CounterView(display2, 同步计数); counterModel.attach(mainDisplay); counterModel.attach(secondaryDisplay); // 3. 获取DOM按钮元素 const btnIncrement document.getElementById(btnIncrement); const btnDecrement document.getElementById(btnDecrement); const btnReset document.getElementById(btnReset); const btnIncrement5 document.getElementById(btnIncrement5); const btnUndo document.getElementById(btnUndo); const btnRedo document.getElementById(btnRedo); const btnClearHistory document.getElementById(btnClearHistory); const strategySelector document.getElementById(strategySelector); // 4. 绑定按钮事件创建并执行命令 btnIncrement.addEventListener(click, () { const command new IncrementCommand(1); counterModel.executeCommand(command); }); btnDecrement.addEventListener(click, () { const command new DecrementCommand(1); counterModel.executeCommand(command); }); btnReset.addEventListener(click, () { const command new ResetCommand(); counterModel.executeCommand(command); }); btnIncrement5.addEventListener(click, () { const command new IncrementCommand(5); // 操作数变为5 counterModel.executeCommand(command); }); // 5. 绑定撤销/重做事件 btnUndo.addEventListener(click, () { counterModel.undo(); }); btnRedo.addEventListener(click, () { counterModel.redo(); }); btnClearHistory.addEventListener(click, () { // 清空历史需要稍微“绕一下”因为模型没有直接暴露_history。 // 一种简单方式重置模型注意也会重置count // 更好的方式是在模型上增加一个 clearHistory 方法。这里为了演示我们简单重置。 const currentCount counterModel.count; const currentStrategy counterModel.strategy; // 重新创建一个新模型并重新附加视图 // 在实际项目中应在CounterModel类中添加 clearHistory 方法。 console.warn(清空历史功能需要扩展模型方法此处演示重置模型。); // 此处省略更优雅的实现建议在CounterModel中增加方法。 }); // 6. 绑定策略切换事件 strategySelector.addEventListener(change, (event) { let newStrategy; switch (event.target.value) { case basic: newStrategy new BasicCountStrategy(); break; case safe: newStrategy new SafeCountStrategy(); break; case circular: newStrategy new CircularCountStrategy(0, 9); break; default: newStrategy new BasicCountStrategy(); } counterModel.strategy newStrategy; // 切换策略后可以用当前值重新通知一次视图确保显示符合新策略例如安全策略下负数被纠正 counterModel.notify({ count: counterModel.count }); }); console.log(应用初始化完成。); // 初始通知一次确保视图显示初始值 counterModel.notify({ count: counterModel.count }); });组装逻辑解析这段代码是典型的“依赖组装”或“组合根”。它创建了所有对象并建立了它们之间的关系模型持有策略视图观察模型按钮触发命令执行。所有具体的类名和依赖关系都集中在这里其他部分都是高内聚、低耦合的模块。这种模式非常有利于测试因为你可以轻松地用模拟对象Mock替换掉真实依赖。5. 模式优势与扩展场景通过上面的完整实现我们已经拥有了一个功能强大且高度灵活的计数器。现在让我们回头审视一下引入这些模式究竟带来了哪些实实在在的好处以及如何应对更复杂的扩展需求。5.1 已实现优势的总结视图与模型彻底解耦CounterView只知道有一个update方法会被调用并传入数据。它不关心数据从哪里来如何计算。我们可以轻松添加第三个、第四个视图比如一个图形化的柱状图BarChartView只需要让它实现Observer接口并注册到模型。模型代码无需任何改动。算法策略可动态替换通过下拉框用户可以在运行时切换计数策略。从模型的视角看它只是换了一个strategy对象所有后续的操作都自动适应新规则。增加一个“百分比计数器”或“随机计数器”策略只需新增一个类。操作历史与撤销/重做命令模式使得记录和回退操作变得非常自然。每个命令对象都是一个独立的数据结构存储了足够的信息来执行和撤销自己。历史栈的管理逻辑被封装在CounterModel中对外提供简单的undo/redo接口。易于单元测试我们可以单独测试BasicCountStrategy.calculate方法是否正确。可以单独测试IncrementCommand的execute和undo是否逻辑正确。可以模拟Mock一个Observer来测试CounterModel的notify是否在正确时机被调用。由于依赖都是注入的测试时可以轻松替换真实DOM或网络请求。代码职责清晰可读性高每个类都有单一、明确的职责。CounterModel管状态和通知CounterView管显示CountStrategy管计算规则Command管操作封装。新人阅读代码时很容易找到相关逻辑所在的位置。5.2 应对更复杂的扩展需求假设产品经理又提出了新需求需求一计数器值需要自动保存到localStorage页面刷新后恢复。原始写法困境需要在每个修改count的地方按钮点击事件都加上localStorage.setItem散落各处容易遗漏。模式化解决方案方案A观察者模式创建一个PersistenceView虽然叫View但它不渲染DOM。它同样实现Observer接口在update方法里将data.count保存到localStorage。然后将其注册到CounterModel。这样任何导致模型状态变化的操作无论是通过按钮命令还是其他方式都会自动触发持久化。新增一个类一行模型代码都不用改。方案B装饰者模式创建一个PersistentCounterModel它“装饰”原有的CounterModel。它内部持有一个CounterModel实例并实现同样的executeCommand、undo、redo等方法。在这些方法中先调用内部模型的对应方法然后再执行持久化逻辑。对于外部调用者来说它就像一个普通的CounterModel但多了自动保存的功能。这种方式更适用于需要对模型行为进行增强或修改的场景。需求二需要为每次操作添加日志发送到服务器进行分析。解决方案与持久化需求类似。可以创建一个LoggingView观察者在update方法中异步发送日志。或者在Command的execute方法中添加日志逻辑。由于命令对象本身就代表了“一次操作”在这里记录操作类型、操作数、时间戳等信息非常合适。命令模式让操作本身成为了可被记录和传递的一等公民。需求三实现一个“宏命令”比如“一键加十”它由10次“加一”命令组成。解决方案这正是命令模式擅长的。我们可以创建一个MacroCommand类它内部维护一个命令数组。它的execute方法会按顺序执行数组中的所有命令undo方法则按相反顺序撤销它们。然后我们就可以将new IncrementCommand(1)重复10次装进一个MacroCommand模型执行这个宏命令即可。这实现了操作的组合和批量执行。需求四在不同页面组件间共享同一个计数器状态。解决方案这引出了另一个重要的模式——单例模式。我们可以确保整个应用中只有一个CounterModel实例。在组装文件app.js中我们将创建好的counterModel实例导出到全局作用域或使用模块导出其他任何需要访问或修改计数器状态的模块都使用这个唯一的实例。这保证了状态的一致性。结合观察者模式所有订阅了该实例的视图都会同步更新。6. 常见问题、调试技巧与性能考量在实际使用和教学过程中我总结了一些容易遇到的问题和值得注意的细节。6.1 常见问题与排查视图没有更新检查点1观察者是否成功注册在CounterModel.attach方法中添加console.log确认视图实例被正确添加到this.observers中。检查点2notify是否被调用在CounterModel的set count的setter或executeCommand方法中确认this.notify()在状态改变后被调用。注意setter中做了新旧值比较如果值没变不会触发通知。检查点3视图的update方法是否正确实现确保视图类继承了Observer并正确定义了update(data)方法。检查方法内的DOM操作是否正确element引用是否有效元素ID是否正确DOM是否已加载。检查点4异步通知问题。我们的notify用了Promise.resolve().then进行异步通知。这意味着视图更新是微任务会稍晚于同步代码执行。如果在这期间有代码依赖于更新后的DOM状态可能会出错。在绝大多数UI场景下这是可接受的但需要知晓这个特性。撤销/重做功能紊乱检查点1命令的execute和undo是否对称确保execute中保存的previousCount能在undo中正确用于恢复状态。一个常见的错误是在execute中直接使用model.count这会导致previousCount保存的不是原始值。检查点2历史栈管理是否正确重点检查executeCommand方法中清空“重做分支”的逻辑 (this._history.splice(this._historyIndex 1))。如果新命令执行后没有清空其后的历史重做时会出现意外行为。检查点3是否使用了setCountDirectly在命令的execute和undo中我们必须调用model.setCountDirectly来修改状态而不是model.executeCommand否则会造成递归调用和死循环。策略切换后行为不符合预期检查点策略对象的calculate方法是否被正确调用在CounterModel.executeCommand中我们通过model.strategy.calculate(...)来委托计算。确保传入的参数当前值、操作类型、操作数是正确的。调试时可以在calculate方法开始处打印日志。注意策略切换本身不会改变当前计数值。例如从“基础策略”切换到“安全策略”如果当前值是-5它不会自动变成0。你需要决定是否在切换策略时自动校正当前值可以在strategy的setter中增加校正逻辑并通知。6.2 性能考量与优化建议观察者数量如果观察者数量非常多例如成百上千notify方法遍历列表可能会成为性能瓶颈。可以考虑批量更新如果状态频繁变化可以引入一个“脏检查”机制或使用requestAnimationFrame对通知进行节流在一帧内只通知一次。按需订阅让视图只订阅它关心的特定状态变化而不是模型的所有变化。这需要更精细的观察者模式变体如“发布/订阅”模式带事件类型。命令历史内存占用如果操作非常频繁且命令对象很大比如保存了复杂的快照历史栈可能会占用大量内存。可以考虑限制历史长度只保留最近N条记录。使用增量快照对于某些操作undo可能需要的信息很少不必保存完整的前状态。策略对象的创建如果策略是无状态的如BasicCountStrategy可以将其实现为单例避免重复创建对象。对于有状态的策略如CircularCountStrategy带有min/max参数则每次都需要新实例。6.3 面向对象与设计模式的取舍最后必须强调一点不要为了使用模式而使用模式。这个计数器示例展示了如何用模式构建一个灵活、可扩展的架构但对于一个真正简单的、永远只有单一视图、不需要撤销、规则固定的计数器最初的几行“面条代码”反而是更优选择——因为它简单、直接、一目了然。设计模式是解决特定复杂问题的工具箱而不是必须遵守的教条。当你预见到需求可能会变化或者代码已经开始出现“坏味道”如重复代码、冗长的条件判断、紧耦合时再考虑引入合适的设计模式进行重构。本次实战的目的正是为了让你在需要这个工具箱时能清楚地知道里面有什么工具以及每件工具该怎么用。通过这个小小的计数器我希望你能感受到良好的设计是如何让代码从容应对变化的这才是面向对象编程和设计模式的精髓所在。