尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从计数器重构看设计模式实战:策略、观察者与装饰者模式

从计数器重构看设计模式实战:策略、观察者与装饰者模式 1. 从一个简单的计数器说起为什么需要设计模式最近在带新人做项目代码评审又看到了那个熟悉的“面条式”计数器。功能很简单就是页面上一个按钮点击一下数字加一。代码大概长这样let count 0; const button document.getElementById(myButton); const display document.getElementById(display); button.addEventListener(click, function() { count; display.textContent count; });看起来没什么问题对吧功能实现了代码也短。但当我问“如果现在需要再加一个按钮点击后数字减一或者再增加一个重置按钮把数字归零你怎么改”新同事想了想说“那就再加两个事件监听器分别操作count变量呗。”这恰恰是问题的开始。随着功能增加代码会迅速膨胀成下面这样let count 0; const button document.getElementById(myButton); const minusButton document.getElementById(minusButton); const resetButton document.getElementById(resetButton); const display document.getElementById(display); button.addEventListener(click, function() { count; display.textContent count; }); minusButton.addEventListener(click, function() { count--; display.textContent count; }); resetButton.addEventListener(click, function() { count 0; display.textContent count; });现在我们有了三个几乎一模一样的display.textContent count;。这违反了 DRYDon‘t Repeat Yourself原则。更麻烦的是如果显示逻辑变了比如我们想同时更新另一个地方的文本或者想在控制台也打印一下就需要修改三个地方。这种代码的“坏味道”在小型项目中或许还能忍受但在稍具规模的工程中它会成为维护的噩梦。这就是为什么我们需要面向对象编程OOP和设计模式。它们不是用来炫技的复杂理论而是为了解决像上面这样代码在演化过程中必然出现的“散弹式修改”和“高耦合”问题。通过 OOP 的封装、继承和多态我们可以把数据和操作数据的行为捆绑在一起而设计模式则是前人总结出来的、针对特定场景的最佳代码组织“套路”。今天我们就用这个最基础的计数器例子抛开那些晦涩的定义看看如何用几种经典的设计模式来重构它让代码变得清晰、健壮且易于扩展。你会发现即使是这么小的功能也能玩出花来。2. 重构第一步用面向对象思维封装计数器在动手应用任何设计模式之前我们得先把代码用面向对象的方式组织起来。这就像盖房子前先打好地基。面向对象的三大支柱——封装、继承、多态——在这里封装是我们最先要用的。2.1 创建一个 Counter 类封装的核心思想是把数据状态和操作这些数据的方法行为捆绑到一个单元里并对外隐藏内部细节。对于计数器它的状态就是当前的计数值count行为就是增加、减少、重置和获取当前值。我们首先创建一个Counter类class Counter { constructor(initialValue 0) { this._count initialValue; // 使用下划线约定表示“私有” } // 增加计数 increment() { this._count; return this.getCount(); // 返回当前值便于链式调用 } // 减少计数 decrement() { this._count--; return this.getCount(); } // 重置计数 reset() { this._count 0; return this.getCount(); } // 获取当前计数只读 getCount() { return this._count; } }为什么这样设计初始值参数constructor(initialValue 0)允许我们在创建计数器时指定起始值这比硬编码0更灵活。“伪私有”属性我们使用this._count而不是this.count。在 ES6 中并没有真正的私有属性ES2022 引入了#前缀的私有字段但兼容性需要考虑。用下划线开头是一种广为人知的约定告诉其他开发者“这个属性请视为内部使用不要直接修改。” 这实现了封装的思想。返回this.getCount()每个修改状态的方法都返回当前值。这是一个小技巧有时可以方便地进行链式调用或直接使用返回值更新UI。独立的getCount方法我们提供了一个专门的方法来获取计数值而不是直接暴露this._count。这为未来可能的逻辑扩展留下了空间比如我们想在获取值时进行日志记录或格式转换。2.2 将 UI 逻辑与业务逻辑解耦现在我们有了一个纯粹的业务逻辑类Counter。它不关心数字显示在网页上、控制台里还是别的地方。这就是关注点分离。我们的 UI 代码事件监听将变得非常干净// 创建计数器实例 const myCounter new Counter(); // 获取DOM元素 const display document.getElementById(display); const incBtn document.getElementById(incBtn); const decBtn document.getElementById(decBtn); const resetBtn document.getElementById(resetBtn); // 初始化显示 display.textContent myCounter.getCount(); // 绑定事件UI层只负责触发业务逻辑和更新视图 incBtn.addEventListener(click, () { const newValue myCounter.increment(); display.textContent newValue; }); decBtn.addEventListener(click, () { const newValue myCounter.decrement(); display.textContent newValue; }); resetBtn.addEventListener(click, () { const newValue myCounter.reset(); display.textContent newValue; });这样做的好处是什么可测试性现在我们可以轻松地测试Counter类的逻辑而不需要操作 DOM。new Counter(5).increment()的结果一定是6。可复用性这个Counter类可以用在任何需要计数器逻辑的地方比如 Node.js 后端、React/Vue 组件状态管理等。维护性如果计数逻辑需要修改比如增加步长参数我们只需要改Counter类。UI 层几乎不用动。注意这里有一个常见的“坑”。在事件监听的回调函数中this的指向会发生变化。我们使用了箭头函数() { ... }因为它没有自己的this会从外层作用域继承从而确保myCounter被正确引用。如果使用function关键字就需要用.bind(myCounter)或者在外层用变量保存this引用。到这一步我们已经用最基本的面向对象思想把一个过程式的脚本改造成了结构清晰的模块。但这只是开始。当需求变得更加复杂时比如我们需要多个不同行为但结构相似的计数器或者需要动态地改变计数器的行为就需要引入设计模式了。3. 模式实战用策略模式实现可切换的计数算法假设我们的产品经理提出了新需求这个计数器不能总是加1减1。有时需要按步长2来增减有时需要按斐波那契数列来增加即下一个值是前两个值之和有时甚至需要随机增加。而且用户可以在运行时动态切换这些计数策略。如果按照最直观的写法我们可能会在Counter类里写一堆if...else或者switchclass Counter { constructor(strategy normal) { this._count 0; this.strategy strategy; } increment() { if (this.strategy normal) { this._count 1; } else if (this.strategy step2) { this._count 2; } else if (this.strategy fibonacci) { // 实现斐波那契逻辑... } // ... 更多策略 return this.getCount(); } // ... 其他方法 }这种写法的弊端非常明显Counter类会变得极其臃肿每增加一种新策略都要修改这个类的increment方法违反了“开闭原则”对扩展开放对修改关闭而且各种算法的实现代码混杂在一起难以阅读和维护。策略模式正是为了解决这类问题而生的。它的核心思想是定义一系列算法将它们一个个封装起来并且使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户在这里就是Counter类。3.1 定义策略接口和具体策略首先我们定义所有计数策略都应该遵守的“契约”——一个策略接口。在 JavaScript 这种动态语言中接口通常通过约定来实现即所有策略对象都实现一个同名的方法比如calculateNext。// 策略接口所有具体策略都必须实现 calculateNext(currentValue) 方法 // 它接收当前值返回计算后的下一个值。 // 具体策略1普通1策略 const NormalStrategy { calculateNext(current) { return current 1; } }; // 具体策略2步长为2的策略 const StepByTwoStrategy { calculateNext(current) { return current 2; } }; // 具体策略3斐波那契策略需要记录前一个值 const FibonacciStrategy (function() { let prev 0; let current 1; return { calculateNext() { const next prev current; prev current; current next; return current; }, // 提供一个重置方法因为斐波那契序列是全局状态 reset() { prev 0; current 1; } }; })(); // 具体策略4随机增加策略 const RandomStrategy { calculateNext(current) { return current Math.floor(Math.random() * 10) 1; // 随机加1-10 } };为什么用对象字面量而不用类对于这种无状态或简单状态的策略对象字面量足够简洁。FibonacciStrategy使用了闭包来维护私有状态prev,current这是一个非常经典的 JavaScript 模式。如果策略很复杂需要多个实例当然也可以使用类。3.2 改造 Counter 类使其支持策略现在我们改造Counter类让它持有一个策略对象的引用并在增加时委托给这个策略。class Counter { constructor(initialValue 0, strategy NormalStrategy) { this._count initialValue; this._strategy strategy; // 持有策略对象的引用 } // 设置策略 setStrategy(strategy) { if (strategy typeof strategy.calculateNext function) { this._strategy strategy; console.log(策略已切换); } else { throw new Error(无效的策略对象必须实现 calculateNext 方法); } } // 获取当前策略 getStrategy() { return this._strategy; } increment() { // 委托给策略对象进行计算 this._count this._strategy.calculateNext(this._count); return this.getCount(); } // decrement 和 reset 也可以定义策略这里为了简化先保持原样 decrement() { this._count--; return this.getCount(); } reset() { this._count 0; // 如果当前策略是 FibonacciStrategy也需要重置其内部状态 if (this._strategy.reset typeof this._strategy.reset function) { this._strategy.reset(); } return this.getCount(); } getCount() { return this._count; } }3.3 在 UI 中动态切换策略现在我们可以在运行时轻松切换计数器的行为了const counter new Counter(0, NormalStrategy); const display document.getElementById(display); const incBtn document.getElementById(incBtn); // 策略选择下拉框 const strategySelect document.getElementById(strategySelect); // 更新显示 function updateDisplay() { display.textContent counter.getCount(); } // 增加按钮事件 incBtn.addEventListener(click, () { counter.increment(); updateDisplay(); }); // 策略切换事件 strategySelect.addEventListener(change, (event) { const selectedStrategy event.target.value; let strategyObj; switch (selectedStrategy) { case normal: strategyObj NormalStrategy; break; case step2: strategyObj StepByTwoStrategy; break; case fibonacci: strategyObj FibonacciStrategy; break; case random: strategyObj RandomStrategy; break; default: strategyObj NormalStrategy; } counter.setStrategy(strategyObj); // 切换策略后可以重置计数器或者保持原值取决于业务需求 // counter.reset(); updateDisplay(); }); // 初始化 updateDisplay();策略模式的优势总结符合开闭原则要增加一个新的计数算法比如“乘以2”你只需要新建一个策略对象如DoubleStrategy而完全不需要修改Counter类的源代码。只需要在 UI 的切换逻辑里加上新选项即可。消除条件判断Counter.increment()方法里再也没有冗长的if-else链代码变得简洁且职责单一。算法复用这些策略对象是独立的可以被其他任何需要类似计算逻辑的模块使用。运行时切换动态改变对象的行为变得非常简单只需调用setStrategy方法。实操心得策略模式并非银弹。如果策略数量很少且几乎不会变化使用简单的条件判断可能更直接。但当你有大量相关算法或者算法需要频繁扩展和替换时策略模式的价值就凸显出来了。在 JavaScript 中由于函数是一等公民策略模式有时可以简化为直接传入一个函数new Counter(0, (current) current 5)。但对于需要维护内部状态如FibonacciStrategy或具有多个方法的复杂策略使用对象封装更为合适。4. 模式实战用观察者模式实现多视图同步更新现在我们的计数器状态变化只会更新一个display元素。但真实场景中一个状态的变化往往需要通知到多个地方。比如计数变化时我们可能想更新网页上的数字显示。同时更新一个进度条的宽度。在控制台打印一条日志。当计数达到某个阈值时弹出提示框。最笨的办法是在Counter.increment()、decrement()、reset()每个方法里都手动去调用更新这些视图的代码。这又回到了最初的高耦合状态。观察者模式也叫发布-订阅模式是解决一对多依赖的利器。它定义了一种对象间的一对多依赖关系当一个对象主题/被观察者的状态发生改变时所有依赖于它的对象观察者都会得到通知并自动更新。在这个例子里Counter就是被观察者Subject而各个需要根据计数更新的 UI 组件或模块就是观察者Observer。4.1 实现一个通用的被观察者基类我们可以先实现一个简单的被观察者基类让Counter继承它这样任何需要被观察的类都可以复用这个逻辑。class Subject { constructor() { this._observers []; // 存储观察者列表 } // 订阅添加观察者 subscribe(observer) { if (observer typeof observer.update function) { this._observers.push(observer); console.log(观察者 ${observer.constructor.name || 匿名} 已订阅); } else { throw new Error(观察者必须实现 update 方法); } } // 取消订阅移除观察者 unsubscribe(observer) { const index this._observers.indexOf(observer); if (index -1) { this._observers.splice(index, 1); console.log(观察者 ${observer.constructor.name || 匿名} 已取消订阅); } } // 通知所有观察者 notify(data) { console.log(主题状态变化正在通知 ${this._observers.length} 个观察者...); this._observers.forEach(observer { try { observer.update(data); } catch (error) { console.error(通知观察者时出错: ${error}); // 通常不应因为一个观察者的错误而影响其他观察者 } }); } }4.2 让 Counter 继承 Subject 并发送通知现在我们重构Counter类让它继承Subject并在其状态改变时调用notify方法。class Counter extends Subject { constructor(initialValue 0, strategy NormalStrategy) { super(); // 调用父类 Subject 的构造函数 this._count initialValue; this._strategy strategy; } // 重写 setStrategy切换策略时也通知观察者 setStrategy(strategy) { if (strategy typeof strategy.calculateNext function) { this._strategy strategy; this.notify({ type: strategy_changed, count: this._count }); } else { throw new Error(无效的策略对象); } } increment() { const oldValue this._count; this._count this._strategy.calculateNext(this._count); // 状态改变通知所有观察者并传递相关信息 this.notify({ type: incremented, oldValue: oldValue, newValue: this._count, count: this._count }); return this.getCount(); } decrement() { const oldValue this._count; this._count--; this.notify({ type: decremented, oldValue: oldValue, newValue: this._count, count: this._count }); return this.getCount(); } reset() { const oldValue this._count; this._count 0; if (this._strategy.reset) { this._strategy.reset(); } this.notify({ type: reset, oldValue: oldValue, newValue: this._count, count: this._count }); return this.getCount(); } getCount() { return this._count; } }关键变化是在每个会改变_count的方法里我们在修改状态后都调用了this.notify(...)并传递了一个包含事件类型和数据的对象。观察者们可以根据这个data对象来决定如何反应。4.3 创建不同的观察者观察者可以是任何实现了update(data)方法的对象。我们来创建几个// 观察者1数字显示组件 class DisplayObserver { constructor(elementId) { this.element document.getElementById(elementId); if (!this.element) { throw new Error(未找到ID为 ${elementId} 的元素); } } update(data) { this.element.textContent data.count; // 可以根据事件类型添加不同样式 if (data.type incremented) { this.element.style.color green; setTimeout(() this.element.style.color , 300); // 闪绿一下 } else if (data.type decremented) { this.element.style.color red; setTimeout(() this.element.style.color , 300); } } } // 观察者2进度条组件假设计数最大为100 class ProgressBarObserver { constructor(elementId, max 100) { this.barElement document.getElementById(elementId); this.max max; if (!this.barElement) { throw new Error(未找到ID为 ${elementId} 的元素); } } update(data) { const percentage Math.min((data.count / this.max) * 100, 100); this.barElement.style.width ${percentage}%; this.barElement.textContent ${Math.round(percentage)}%; } } // 观察者3控制台日志器 class ConsoleLoggerObserver { constructor(name Counter) { this.name name; } update(data) { console.log([${this.name}] ${data.type}: ${data.oldValue} - ${data.newValue}); } } // 观察者4阈值报警器 class AlertThresholdObserver { constructor(threshold) { this.threshold threshold; this.hasAlerted false; // 防止重复报警 } update(data) { if (data.count this.threshold !this.hasAlerted) { alert(警告计数器已达到或超过阈值 ${this.threshold}!); this.hasAlerted true; } else if (data.count this.threshold) { this.hasAlerted false; // 重置报警状态 } } }4.4 组装一切松耦合的应用程序现在我们的 UI 初始化代码变得异常清晰和简洁// 1. 创建被观察者主题 const counter new Counter(0, NormalStrategy); // 2. 创建各个观察者 const displayObserver new DisplayObserver(display); const progressBarObserver new ProgressBarObserver(progressBar, 50); const consoleObserver new ConsoleLoggerObserver(我的计数器); const alertObserver new AlertThresholdObserver(10); // 3. 让观察者订阅主题 counter.subscribe(displayObserver); counter.subscribe(progressBarObserver); counter.subscribe(consoleObserver); counter.subscribe(alertObserver); // 4. UI控件只需要操作 counter 对象 document.getElementById(incBtn).addEventListener(click, () counter.increment()); document.getElementById(decBtn).addEventListener(click, () counter.decrement()); document.getElementById(resetBtn).addEventListener(click, () counter.reset()); // 5. 策略切换逻辑策略切换也会触发通知 document.getElementById(strategySelect).addEventListener(change, (event) { // ... 同之前的策略选择逻辑最后调用 counter.setStrategy counter.setStrategy(selectedStrategyObj); });观察者模式的优势与陷阱优势彻底解耦Counter完全不知道有哪些观察者也不知道它们具体做了什么。它只负责在状态变化时发出通知。动态关系可以在运行时轻松地添加或移除观察者subscribe/unsubscribe系统弹性极佳。广播通信一对多的通知机制非常高效。陷阱与注意事项通知顺序观察者被通知的顺序通常就是它们被添加的顺序但这个顺序不应影响业务逻辑。如果顺序重要可能需要更复杂的优先级机制。循环依赖如果观察者在update方法中又调用了主题的方法可能会引发无限循环的通知。性能开销如果观察者数量巨大或者update方法执行很慢notify过程会成为性能瓶颈。需要谨慎设计。内存泄漏如果观察者对象不再使用但忘记取消订阅主题会一直持有对它的引用导致其无法被垃圾回收。在 SPA单页应用中组件销毁时务必取消订阅。实操心得观察者模式是前端框架如 React、Vue响应式系统的基石。在实际项目中我们很少需要自己从头实现一个Subject类因为已经有RxJS、EventEmitter等成熟的库。但理解其原理至关重要。在计数器这个例子里我们手动实现它能让你深刻体会到状态与视图分离的威力。当你下次使用 Vue 的watch或 React 的useEffect时你会明白你正在使用一个更高级、更声明式的“观察者模式”。5. 模式实战用装饰者模式动态扩展计数器功能需求又来了。现在我们需要为计数器添加一些“横切关注点”的功能比如历史记录记录每一次计数操作增加、减少、重置的历史并可以撤销。持久化将当前计数自动保存到localStorage页面刷新后能恢复。操作限制限制计数器在某个范围内如 -10 到 10超出范围的操作无效并给出提示。同样我们当然可以直接修改Counter类的源代码在每个方法里添加对应的逻辑。但这会让Counter的核心职责计数与这些辅助功能混杂在一起违反了“单一职责原则”。而且这些功能可能只有特定场景需要我们希望能灵活地给一个计数器“装上”或“卸下”这些功能。装饰者模式允许我们动态地给一个对象添加额外的职责而无需修改其源代码。它通过将对象包装在装饰者对象中来实现这些装饰者对象与原始对象拥有相同的接口因此对客户端代码来说是透明的。5.1 理解装饰者模式的结构装饰者模式的核心是“包装”。我们有一个“组件”接口Counter有一个“具体组件”基础的Counter类还有多个“装饰者”。每个装饰者都持有一个组件对象的引用并在调用其方法前后执行自己的逻辑。在 JavaScript 中由于类和对象可以动态修改实现装饰者模式有多种方式。这里我们采用“组合”的方式创建一个装饰者基类它同样实现Counter的公共接口increment,decrement,reset,getCount,setStrategy,subscribe等并持有一个被装饰的counter实例。首先我们可能需要明确一下Counter的公共接口。为了装饰方便我们假设所有Counter实例包括装饰过的都至少有以下方法increment(),decrement(),reset(),getCount()。策略模式和观察者模式的方法setStrategy,subscribe等对于装饰者来说可能是可选的或需要透传。5.2 实现装饰者基类这个基类会接收一个Counter实例并实现所有Counter的方法默认行为就是转发给被装饰的实例。class CounterDecorator { constructor(counter) { if (!counter || typeof counter.increment ! function) { throw new Error(装饰者必须包装一个有效的 Counter 实例); } this._counter counter; } // 转发所有核心方法 increment() { return this._counter.increment(); } decrement() { return this._counter.decrement(); } reset() { return this._counter.reset(); } getCount() { return this._counter.getCount(); } // 如果被装饰的 counter 有 setStrategy 和 subscribe 方法也进行转发 // 这是一个灵活的处理确保装饰后的对象仍然可用 setStrategy(strategy) { if (this._counter.setStrategy) { return this._counter.setStrategy(strategy); } // 如果底层 counter 不支持策略可以忽略或抛出错误 console.warn(底层 Counter 不支持 setStrategy); } subscribe(observer) { if (this._counter.subscribe) { return this._counter.subscribe(observer); } console.warn(底层 Counter 不支持 subscribe); } unsubscribe(observer) { if (this._counter.unsubscribe) { return this._counter.unsubscribe(observer); } console.warn(底层 Counter 不支持 unsubscribe); } // 提供一个方法获取被装饰的原始对象用于特殊操作 getWrappedCounter() { return this._counter; } }5.3 实现具体装饰者现在我们可以创建具体的装饰者来添加功能了。每个装饰者都继承自CounterDecorator并重写需要增强的方法。装饰者1历史记录装饰者class HistoryDecorator extends CounterDecorator { constructor(counter) { super(counter); this._history []; // 记录操作历史 [{type, oldValue, newValue}] this._historyIndex -1; // 当前历史指针 } increment() { const oldValue this._counter.getCount(); const result super.increment(); // 调用父类方法即转发给被装饰的counter this._recordHistory(increment, oldValue, this.getCount()); return result; } decrement() { const oldValue this._counter.getCount(); const result super.decrement(); this._recordHistory(decrement, oldValue, this.getCount()); return result; } reset() { const oldValue this._counter.getCount(); const result super.reset(); this._recordHistory(reset, oldValue, this.getCount()); return result; } // 私有方法记录历史 _recordHistory(type, oldValue, newValue) { // 如果当前指针不在历史末尾则截断后面的历史例如撤销后执行了新操作 if (this._historyIndex this._history.length - 1) { this._history this._history.slice(0, this._historyIndex 1); } this._history.push({ type, oldValue, newValue, timestamp: Date.now() }); this._historyIndex; console.log(历史记录: ${type} ${oldValue} - ${newValue}); } // 新增方法撤销 undo() { if (this._historyIndex 0) { console.log(没有可撤销的操作); return false; } const lastAction this._history[this._historyIndex]; // 将值回退到操作前的状态 this._counter._count lastAction.oldValue; // 注意这里直接修改了内部状态破坏了封装。更好的做法是让Counter支持setCount。 this._historyIndex--; console.log(撤销: ${lastAction.type}, 当前值: ${this.getCount()}); // 通知观察者如果被装饰的counter支持 if (this._counter.notify) { this._counter.notify({ type: undo, oldValue: lastAction.newValue, newValue: lastAction.oldValue, count: this.getCount() }); } return true; } // 新增方法重做 redo() { if (this._historyIndex this._history.length - 1) { console.log(没有可重做的操作); return false; } this._historyIndex; const nextAction this._history[this._historyIndex]; this._counter._count nextAction.newValue; console.log(重做: ${nextAction.type}, 当前值: ${this.getCount()}); if (this._counter.notify) { this._counter.notify({ type: redo, oldValue: nextAction.oldValue, newValue: nextAction.newValue, count: this.getCount() }); } return true; } // 新增方法获取历史 getHistory() { return this._history.slice(0, this._historyIndex 1); } }装饰者2持久化装饰者class PersistenceDecorator extends CounterDecorator { constructor(counter, storageKey counter_value) { super(counter); this._storageKey storageKey; // 从存储中加载初始值 const savedValue localStorage.getItem(this._storageKey); if (savedValue ! null) { const parsedValue parseInt(savedValue, 10); if (!isNaN(parsedValue)) { this._counter._count parsedValue; // 同样直接修改内部状态 console.log(从 localStorage 加载值: ${parsedValue}); } } } increment() { const result super.increment(); this._saveToStorage(); return result; } decrement() { const result super.decrement(); this._saveToStorage(); return result; } reset() { const result super.reset(); this._saveToStorage(); return result; } // 私有方法保存到存储 _saveToStorage() { localStorage.setItem(this._storageKey, this.getCount().toString()); console.log(值已保存到 localStorage: ${this.getCount()}); } // 新增方法清除存储 clearStorage() { localStorage.removeItem(this._storageKey); console.log(localStorage 数据已清除); } }装饰者3范围限制装饰者class RangeLimitDecorator extends CounterDecorator { constructor(counter, min -Infinity, max Infinity) { super(counter); this._min min; this._max max; // 初始化时确保当前值在范围内 this._clampValue(); } increment() { const oldValue this.getCount(); if (oldValue this._max) { console.warn(已达到最大值 ${this._max}无法增加); // 可以触发一个通知或回调 if (this._counter.notify) { this._counter.notify({ type: limit_reached, direction: max, count: oldValue }); } return oldValue; // 不执行实际增加操作 } return super.increment(); } decrement() { const oldValue this.getCount(); if (oldValue this._min) { console.warn(已达到最小值 ${this._min}无法减少); if (this._counter.notify) { this._counter.notify({ type: limit_reached, direction: min, count: oldValue }); } return oldValue; } return super.decrement(); } reset() { // 重置到0但0可能不在[min, max]范围内需要处理 const result super.reset(); this._clampValue(); // 确保重置后的值在范围内 return this.getCount(); } setStrategy(strategy) { // 切换策略后也需要确保当前值在新策略下是有效的这里简化处理 const result super.setStrategy(strategy); this._clampValue(); return result; } // 私有方法将当前值钳制在[min, max]范围内 _clampValue() { const current this._counter.getCount(); if (current this._min) { this._counter._count this._min; console.log(值 ${current} 小于最小值 ${this._min}已调整为 ${this._min}); } else if (current this._max) { this._counter._count this._max; console.log(值 ${current} 大于最大值 ${this._max}已调整为 ${this._max}); } } }5.4 像搭积木一样组合功能装饰者模式的魔力在于你可以像俄罗斯套娃一样一层层地包装计数器动态地组合出你需要的功能。// 1. 创建一个基础计数器带有观察者功能 const basicCounter new Counter(5, NormalStrategy); basicCounter.subscribe(new ConsoleLoggerObserver(基础计数器)); // 2. 给它加上历史记录功能 const counterWithHistory new HistoryDecorator(basicCounter); // 3. 在历史记录的基础上再加上持久化功能 const counterWithHistoryAndPersistence new PersistenceDecorator(counterWithHistory, myCounter); // 4. 最后再加上范围限制功能-10 到 10 const fullyDecoratedCounter new RangeLimitDecorator(counterWithHistoryAndPersistence, -10, 10); // 现在fullyDecoratedCounter 拥有所有功能 // 对客户端代码来说它仍然是一个“Counter”对象接口一致。 // UI 操作现在针对这个装饰后的计数器 const incBtn document.getElementById(incBtn); const decBtn document.getElementById(decBtn); const undoBtn document.getElementById(undoBtn); const redoBtn document.getElementById(redoBtn); incBtn.addEventListener(click, () { fullyDecoratedCounter.increment(); updateDisplay(); }); decBtn.addEventListener(click, () { fullyDecoratedCounter.decrement(); updateDisplay(); }); undoBtn.addEventListener(click, () { // 注意undo 是 HistoryDecorator 特有的方法 // 我们需要通过 getWrappedCounter 找到 HistoryDecorator 实例来调用 // 更好的设计是让装饰器也实现 undo/redo 接口这里为了演示简化了 if (fullyDecoratedCounter.getWrappedCounter() instanceof HistoryDecorator) { fullyDecoratedCounter.getWrappedCounter().undo(); updateDisplay(); } }); // ... 其他按钮事件 function updateDisplay() { document.getElementById(display).textContent fullyDecoratedCounter.getCount(); }装饰者模式的优缺点优点符合开闭原则无需修改原有类就能扩展新功能。组合优于继承通过组合对象的方式可以动态、灵活地添加功能避免了继承可能导致的类爆炸问题。职责清晰每个装饰者类只关注一个特定的附加功能。缺点与注意事项复杂性会引入大量小对象调试时调用栈可能很深不易理解。初始化复杂组装多层装饰者时代码可能显得冗长。接口一致性要求装饰者和被装饰对象有相同的接口。如果原始对象的方法很多装饰者基类需要转发所有方法代码会显得冗余。TypeScript 的接口和抽象类可以更好地解决这个问题。访问内部状态如例子所示装饰者有时需要访问被装饰对象的内部状态如_count这破坏了封装性。一种改进方案是为Counter提供更精细的公共方法如setCountAndNotify或者使用 Symbol 作为私有属性键。实操心得装饰者模式在前端中非常常见。例如React 的高阶组件HOC、ES7 的装饰器语法decorator都是这种思想的应用。当你发现需要给一个对象动态添加一些与核心逻辑关系不大的“边角料”功能时就可以考虑装饰者模式。但切记不要过度使用否则代码会变得像洋葱一样层层包裹难以追踪。6. 总结与模式选择思考通过这个简单的计数器我们实践了三种经典的设计模式策略模式、观察者模式和装饰者模式。从一个几十行的过程式脚本到如今这个结构清晰、功能强大、扩展性极强的模块化代码我们看到了设计模式如何将混乱的代码梳理得井井有条。让我们回顾一下它们各自解决的核心问题策略模式解决了算法族的选择问题。当你有多种完成同一任务的不同方式并且希望能在运行时灵活切换时就用它。它让算法的定义与使用它的客户端解耦。观察者模式解决了一对多的状态同步问题。当一个对象的状态改变需要自动通知其他多个对象时它是首选。它实现了主题与观察者之间的松耦合。装饰者模式解决了动态扩展对象功能的问题。当你需要给对象添加一些额外的职责但又不想通过继承导致子类泛滥时它提供了灵活的替代方案。那么在实际项目中该如何选择没有银弹。模式是工具而不是目标。我的经验是先写“笨”代码不要一开始就想着用模式。像文章开头那样先把功能用最简单直接的方式实现出来。这是为了快速验证需求。识别“坏味道”当代码开始出现重复DRY原则被违反、一个类的职责过多单一职责原则被违反、或者修改一个地方需要动多个毫不相关的模块时耦合过高就是重构的信号。对号入座根据你识别出的具体问题去匹配设计模式。是算法多变考虑策略。是状态变化需要广播考虑观察者。是想动态添加功能而不改原类考虑装饰者。权衡复杂度设计模式通常会引入额外的抽象层增加代码的理解成本。如果项目很小或者某个功能几乎不可能变化那么简单的if-else或直接修改可能更合适。不要为了用模式而用模式。最后关于这个计数器例子我个人在实际编码中还有两点体会模式的组合使用我们最后的fullyDecoratedCounter实际上是策略、观察者、装饰者三种模式组合的产物。这说明模式不是孤立的它们可以协同工作构建出复杂而优雅的系统。JavaScript 的特性JavaScript 的动态性和函数是一等公民的特性让一些模式的实现比其他静态语言更灵活、更简洁比如策略模式有时就是一个函数参数。充分利用语言特性而不是生搬硬套 GoF 书中的类图才是 JavaScript 设计模式的正确打开方式。希望这个从零开始的计数器之旅能让你感受到设计模式不是空中楼阁而是源于解决实际编码痛点的最佳实践。下次当你面对一团乱麻的代码时不妨想想这里是不是藏着一个策略、一个观察者或者一个装饰者
返回列表