阿里云主机折上折
  • 微信号
Current Site:Index > Common design pattern misuse and anti-patterns

Common design pattern misuse and anti-patterns

Author:Chuan Chen 阅读数:12097人阅读 分类: JavaScript

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.