Common design pattern misuse and anti-patterns
Typical Scenarios of Design Pattern Misuse
Design patterns are born to solve specific problems, but mechanical application often backfires. The abuse of the Singleton pattern in JavaScript is particularly common, where developers often mistake module globalization for singleton implementation:
// Anti-pattern example: Pseudo-singleton
const pseudoSingleton = {
data: [],
add(item) {
this.data.push(item)
}
}
// Module A modifies data
pseudoSingleton.add('A')
// Module B accidentally overwrites data
pseudoSingleton.data = null
This implementation lacks true instance control and cannot prevent direct modification of internal state. A more reasonable approach is to use closures to protect the instance:
const RealSingleton = (() => {
let instance
class Singleton {
constructor() {
this.data = []
}
add(item) {
this.data.push(item)
}
}
return {
getInstance() {
if (!instance) {
instance = new Singleton()
}
return instance
}
}
})()
The Over-Subscription Trap of the Observer Pattern
In event-driven architectures, the Observer pattern often leads to memory leaks due to untimely unsubscription. A typical scenario is a global event bus in SPAs:
// Dangerous implementation
class EventBus {
listeners = {}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
emit(event, data) {
(this.listeners[event] || []).forEach(cb => cb(data))
}
}
// Usage in a component
mounted() {
eventBus.on('dataUpdate', this.handleData)
}
// Forgetting to unsubscribe when the component is destroyed -> Memory leak
An improved solution should enforce an unsubscription mechanism:
class SafeEventBus {
listeners = new Map()
on(event, callback) {
const callbacks = this.listeners.get(event) || new Set()
callbacks.add(callback)
this.listeners.set(event, callbacks)
return () => this.off(event, callback) // Returns an unsubscribe function
}
off(event, callback) {
const callbacks = this.listeners.get(event)
if (callbacks) {
callbacks.delete(callback)
}
}
}
Conditional Explosion in the Strategy Pattern
The Strategy pattern is meant to simplify conditional branches, but improper implementation can lead to bloated strategy classes:
// Anti-pattern: Strategy class contains too many conditions
class PaymentStrategy {
pay(method, amount) {
switch(method) {
case 'alipay':
return this.processAlipay(amount)
case 'wechat':
return this.processWeChat(amount)
case 'creditCard':
return this.processCreditCard(amount)
// Adding new payment methods requires modifying the class
}
}
}
It should be split into independent strategy objects:
const strategies = {
alipay: (amount) => { /* Specific implementation */ },
wechat: (amount) => { /* Specific implementation */ },
creditCard: (amount) => { /* Specific implementation */ }
}
function processPayment(method, amount) {
return strategies[method]?.(amount) ?? defaultStrategy(amount)
}
Performance Costs of the Decorator Pattern
Abusing the Decorator pattern can lead to overly deep call stacks, especially in React higher-order components:
// Multiple layers of decoration make the component tree hard to debug
const EnhancedComponent = withRouter(
connect(mapStateToProps)(
withStyles(styles)(
memo(BaseComponent)
)
)
)
Modern React prefers Hook composition:
function SmartComponent() {
const router = useRouter()
const data = useSelector(mapStateToProps)
const classes = useStyles(styles)
return <BaseComponent {...props} />
}
Over-Abstraction in Factory Methods
Factory methods can add unnecessary complexity in simple object creation scenarios:
// Unnecessary factory
class ButtonFactory {
createButton(type) {
switch(type) {
case 'primary':
return new PrimaryButton()
case 'secondary':
return new SecondaryButton()
default:
return new DefaultButton()
}
}
}
// Direct instantiation is clearer
const buttonMap = {
primary: <PrimaryButton />,
secondary: <SecondaryButton />,
default: <DefaultButton />
}
function Button({ type = 'default' }) {
return buttonMap[type] || buttonMap.default
}
Over-Encapsulation in the Command Pattern
Encapsulating simple operations as command objects can reduce readability:
// Over-engineered Command pattern
class Command {
execute() {}
}
class SaveCommand extends Command {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.save()
}
}
// Usage
const command = new SaveCommand(editor)
command.execute()
// Direct invocation is clearer
editor.save()
The Command pattern should only be used for advanced features like undo/redo.
Interface Pollution in the Adapter Pattern
Adapters can mask underlying interface design issues:
// Anti-pattern: Using adapters to hide design flaws
class BadAPI {
getData() {
return fetch('/legacy-endpoint')
.then(res => res.json())
.then(data => ({
items: data.records,
meta: data.info
}))
}
}
// A better approach is to fix the API design directly
class GoodAPI {
async getItems() {
const res = await fetch('/modern-endpoint')
return res.json()
}
}
Inheritance Pitfalls in the Template Method Pattern
Traditional implementations create tight coupling between subclasses and parent classes:
// Fragile Template Method
class DataProcessor {
process() {
this.validate()
this.transform()
this.save()
}
validate() { /* Default implementation */ }
transform() { /* Abstract method */ }
save() { /* Default implementation */ }
}
// Using composition strategies is more flexible
function createProcessor({ validate, transform, save }) {
return {
process() {
validate?.()
transform()
save?.()
}
}
}
State Explosion in the State Pattern
Complex state machines can lead to a proliferation of state classes:
// Too many state classes
class Order {
state = new DraftState(this)
setState(state) {
this.state = state
}
}
class DraftState { /* ... */ }
class PendingState { /* ... */ }
class PaidState { /* ... */ }
class ShippedState { /* ... */ }
class CancelledState { /* ... */ }
Consider using a state table-driven approach:
const stateMachine = {
draft: {
submit: 'pending',
cancel: 'cancelled'
},
pending: {
pay: 'paid',
cancel: 'cancelled'
},
paid: {
ship: 'shipped'
}
}
function transition(state, action) {
return stateMachine[state]?.[action] || state
}
God Object in the Mediator Pattern
Mediators can evolve into overly centralized control hubs:
// Overburdened mediator
class ChatRoom {
constructor() {
this.users = []
}
register(user) {
this.users.push(user)
user.room = this
}
send(message, from) {
this.users.forEach(user => {
if (user !== from) {
user.receive(message)
}
})
}
// Gradually adding more business logic...
kickUser() {}
changeTopic() {}
setRules() {}
}
Follow the Single Responsibility Principle to split functionality:
class UserManager {
addUser() {}
removeUser() {}
}
class MessageDispatcher {
broadcast() {}
privateMessage() {}
}
class RoomPolicy {
setRules() {}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:设计模式与代码可维护性的关系
下一篇:如何选择合适的设计模式