这篇文章是关于我们许多人在 React 和前端开发中遇到的一个问题(有时甚至没有意识到这是一个问题):在不同的组件、钩子、实用程序等中实现了一段逻辑。
让我们深入了解问题的详细信息以及如何解决它。正如标题所暗示的,我们将使用策略模式来解决它。
问题:霰弹枪手术
Shotgun Surgery是一种代码味道,其中进行任何修改都需要对许多不同的地方进行许多小的更改。
(图片来源:https ://refactoring.guru/smells/shotgun-surgery )
这怎么会发生在一个项目中?假设我们需要为产品实施定价卡,我们根据客户的来源调整价格、货币、折扣策略和消息:
我们大多数人可能会按如下方式实施定价卡:
- 组件:
PricingCard
,PricingHeader
,PricingBody
。 - 效用函数:(
getDiscountMessage
在utils/discount.ts中),formatPriceByCurrency
(在utils/price.ts 中)。 - 该
PricingBody
组件还计算最终价格。
这是完整的实现:
现在假设我们需要更改一个国家/地区的定价计划,或为另一个国家/地区添加新的定价计划。您将如何处理上述实施?您必须至少修改 3 个地方并向已经凌乱的if-else
块添加更多条件:
- 修改
PricingBody
组件。 - 修改
getDiscountMessage
函数。 - 修改
formatPriceByCurrency
函数。
如果您已经听说过 SOLID,那么我们已经违反了前 2 个原则:单一职责原则和开闭原则。
解决方案:策略模式
策略模式非常简单。我们可以简单的理解为我们每个国家的定价方案都是一个策略。在那个策略类中,我们实现了该策略的所有相关逻辑。
假设您熟悉 OOP,我们可以有一个PriceStrategy
实现共享/公共逻辑的抽象类 ( ),然后具有不同逻辑的策略将继承该抽象类。PriceStrategy
抽象类如下所示:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">import</span> <span style="color:var(--syntax-text-color)">{</span> <span style="color:var(--syntax-name-color)">Country</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">Currency</span> <span style="color:var(--syntax-text-color)">}</span> <span style="color:var(--syntax-declaration-color)">from</span> <span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-string-color)">../../types</span><span style="color:var(--syntax-string-color)">'</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">abstract</span> <span style="color:var(--syntax-declaration-color)">class</span> <span style="color:var(--syntax-name-color)">PriceStrategy</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">protected</span> <span style="color:var(--syntax-name-color)">country</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">Country</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">Country</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">AMERICA</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">protected</span> <span style="color:var(--syntax-name-color)">currency</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">Currency</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-name-color)">Currency</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">USD</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-declaration-color)">protected</span> <span style="color:var(--syntax-name-color)">discountRatio</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-literal-color)">0</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-name-color)">getCountry</span><span style="color:var(--syntax-text-color)">():</span> <span style="color:var(--syntax-name-color)">Country</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">country</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">formatPrice</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">number</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-declaration-color)">string</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">currency</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">toLocaleString</span><span style="color:var(--syntax-text-color)">()].</span><span style="color:var(--syntax-name-color)">join</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-string-color)">''</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">getDiscountAmount</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">number</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-declaration-color)">number</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-name-color)">price</span> <span style="color:var(--syntax-error-color)">*</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">discountRatio</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">getFinalPrice</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">number</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-declaration-color)">number</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-name-color)">price</span> <span style="color:var(--syntax-error-color)">-</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">getDiscountAmount</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">shouldDiscount</span><span style="color:var(--syntax-text-color)">():</span> <span style="color:var(--syntax-name-color)">boolean</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">discountRatio</span> <span style="color:var(--syntax-error-color)">></span> <span style="color:var(--syntax-literal-color)">0</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">getDiscountMessage</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">number</span><span style="color:var(--syntax-text-color)">):</span> <span style="color:var(--syntax-declaration-color)">string</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">formattedDiscountAmount</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">formatPrice</span><span style="color:var(--syntax-text-color)">(</span>
<span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">getDiscountAmount</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">)</span>
<span style="color:var(--syntax-text-color)">);</span>
<span style="color:var(--syntax-declaration-color)">return</span> <span style="color:var(--syntax-string-color)">`It's lucky that you come from </span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-declaration-color)">this</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">country</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-string-color)">, because we're running a program that discounts the price by </span><span style="color:var(--syntax-text-color)">${</span><span style="color:var(--syntax-name-color)">formattedDiscountAmount</span><span style="color:var(--syntax-text-color)">}</span><span style="color:var(--syntax-string-color)">.`</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-declaration-color)">export</span> <span style="color:var(--syntax-declaration-color)">default</span> <span style="color:var(--syntax-name-color)">PriceStrategy</span><span style="color:var(--syntax-text-color)">;</span>
</code></span></span>
我们只需将实例化的策略作为 prop 传递给PricingCard
组件:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-text-color)"><</span><span style="color:var(--syntax-name-color)">PricingCard</span> <span style="color:var(--syntax-name-color)">price</span><span style="color:var(--syntax-text-color)">=</span><span style="color:var(--syntax-string-color)">{</span><span style="color:var(--syntax-literal-color)">7669</span><span style="color:var(--syntax-string-color)">}</span> <span style="color:var(--syntax-name-color)">strategy</span><span style="color:var(--syntax-text-color)">=</span><span style="color:var(--syntax-string-color)">{</span><span style="color:var(--syntax-declaration-color)">new</span> <span style="color:var(--syntax-name-color)">JapanPriceStrategy</span><span style="color:var(--syntax-text-color)">()</span><span style="color:var(--syntax-string-color)">}</span> <span style="color:var(--syntax-text-color)">/></span>
</code></span></span>
道具PricingCard
定义为:
<span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">interface</span> <span style="color:var(--syntax-name-color)">PricingCardProps</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-text-color)">price</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-declaration-color)">number</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">strategy</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-name-color)">PriceStrategy</span><span style="color:var(--syntax-text-color)">;</span>
<span style="color:var(--syntax-text-color)">}</span>
</code></span></span>
同样,如果您了解 OOP,那么我们不仅在使用继承,而且还在此处使用多态性。
这是解决方案的完整实现:
让我们再次问同样的问题:我们如何为新的国家/地区添加新的定价计划?使用这个解决方案,我们只需要添加一个新的策略类,而不需要修改任何现有代码。通过这样做,我们也满足了 SOLID。
结论
因此,通过在我们的 React 代码库中检测代码异味——Shotgun Surgery,我们应用了一种设计模式——策略模式——来解决它。我们的代码结构来自于:
对此:
现在我们的逻辑存在于一个地方,不再分布在许多地方。
如果您对设计模式和架构以及如何使用它们来解决前端世界中的问题感兴趣,请务必给我点赞和关注。