Skip to content

Design Patterns Introduction and Classification: A Best Practices Guide for Software Engineering โ€‹

What Are Design Patterns? โ€‹

Design patterns are typical solutions to common problems in software design. They are summaries of design experience that have been used repeatedly, known by many, cataloged, and refined over time. Using design patterns makes code easier for others to understand and ensures code reliability, reusability, and maintainability.

Imagine you're building a house. If you're building for the first time, you might make many mistakes: windows that are too large, poorly designed electrical circuits, leaky pipes, and so on. But if you have a set of mature architectural blueprints and construction standards, you can avoid these problems and build a more stable, practical house.

Software design patterns are like best practices in the construction industryโ€”they solve common problems in software development:

javascript
// Code without design patterns
class PaymentProcessor {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }

  processPayment(amount) {
    switch (this.paymentType) {
      case "paypal":
        console.log("PayPal payment:", amount);
        // PayPal implementation
        break;

      case "stripe":
        console.log("Stripe payment:", amount);
        // Stripe implementation
        break;

      case "square":
        console.log("Square payment:", amount);
        // Square implementation
        break;

      case "creditcard":
        console.log("Credit card payment:", amount);
        // Credit card implementation
        break;

      default:
        throw new Error("Unsupported payment method");
    }
  }
}

// When adding a new payment method, we need to modify the PaymentProcessor class
// This violates the Open-Closed Principle (open for extension, closed for modification)
javascript
// Code after applying Strategy Pattern
class PayPalPayment {
  process(amount) {
    console.log("PayPal payment:", amount);
    // PayPal implementation
  }
}

class StripePayment {
  process(amount) {
    console.log("Stripe payment:", amount);
    // Stripe implementation
  }
}

class PaymentProcessor {
  constructor(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  setPaymentStrategy(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  processPayment(amount) {
    this.paymentStrategy.process(amount);
  }
}

// Adding a new payment method doesn't require modifying existing code, just implement a new strategy class
class SquarePayment {
  process(amount) {
    console.log("Square payment:", amount);
    // Square implementation
  }
}

// Usage example
const processor = new PaymentProcessor(new PayPalPayment());
processor.processPayment(100); // PayPal payment:100

processor.setPaymentStrategy(new StripePayment());
processor.processPayment(200); // Stripe payment:200

Origin and Evolution of Design Patterns โ€‹

The concept of design patterns was first introduced by architect Christopher Alexander in "A Pattern Language" for architecture. It was later brought into software engineering by Erich Gamma and three other authors (known as the "GoF - Gang of Four") in their book "Design Patterns: Elements of Reusable Object-Oriented Software".

The GoF's 23 classic design patterns include:

  • Creational Patterns: 5 patterns for object creation mechanisms
  • Structural Patterns: 7 patterns for composing classes and objects into larger structures
  • Behavioral Patterns: 11 patterns for responsibility distribution between objects

As software engineering has evolved, the concept of design patterns has continued to develop. Particularly in front-end development, many design patterns have emerged specifically for JavaScript and particular frameworks.

Core Value of Design Patterns โ€‹

1. Code Reusability โ€‹

Design patterns provide proven solutions, avoiding reinventing the wheel:

javascript
// Singleton Pattern - Ensure a class has only one instance
class DatabaseConnection {
  constructor() {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = this;
      this.connection = this.connect();
    }
    return DatabaseConnection.instance;
  }

  connect() {
    // Database connection implementation
    return { connected: true, id: Date.now() };
  }

  query(sql) {
    return this.connection.query(sql);
  }
}

// Reuse the same database connection throughout the application
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true, ensures only one connection

2. Maintainability โ€‹

Good design patterns make code structure clear and easy to understand and modify:

javascript
// Observer Pattern - Implement publish-subscribe mechanism
class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }

  emit(event, data) {
    if (this.listeners[event]) {
      this.listeners[event].forEach((callback) => callback(data));
    }
  }

  off(event, callback) {
    if (this.listeners[event]) {
      this.listeners[event] = this.listeners[event].filter(
        (cb) => cb !== callback
      );
    }
  }
}

// Application in React components
function useShoppingCart() {
  const [items, setItems] = useState([]);
  const eventEmitter = useRef(new EventEmitter());

  useEffect(() => {
    const handleAddItem = (item) => {
      setItems((prev) => [...prev, item]);
    };

    eventEmitter.current.on("add-item", handleAddItem);

    return () => {
      eventEmitter.current.off("add-item", handleAddItem);
    };
  }, []);

  return {
    items,
    addItem: (item) => eventEmitter.current.emit("add-item", item),
  };
}

3. Extensibility โ€‹

Design patterns make systems easier to extend without modifying existing code:

javascript
// Decorator Pattern - Dynamically add functionality
class BasicCoffee {
  cost() {
    return 10;
  }

  description() {
    return "Basic Coffee";
  }
}

class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  cost() {
    return this.coffee.cost();
  }

  description() {
    return this.coffee.description();
  }
}

class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 2;
  }

  description() {
    return this.coffee.description() + " + Milk";
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }

  description() {
    return this.coffee.description() + " + Sugar";
  }
}

// Can combine different decorators freely
let coffee = new BasicCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.description()); // Basic Coffee + Milk + Sugar
console.log(coffee.cost()); // 13

Design Pattern Classification System โ€‹

Creational Patterns โ€‹

Creational patterns focus on object creation processes, separating object creation from usage to enhance system flexibility and maintainability.

Key Characteristics:

  • Encapsulate creation logic
  • Reduce creation complexity
  • Improve creation consistency

Common Scenarios:

javascript
// Factory Pattern - Create different types of charts based on configuration
class ChartFactory {
  static createChart(type, data) {
    switch (type) {
      case "line":
        return new LineChart(data);
      case "bar":
        return new BarChart(data);
      case "pie":
        return new PieChart(data);
      default:
        throw new Error(`Unsupported chart type: ${type}`);
    }
  }
}

// Use factory to create charts
const chart = ChartFactory.createChart("line", salesData);
chart.render();

Structural Patterns โ€‹

Structural patterns focus on the composition of classes and objects, used to create larger structures while maintaining structural flexibility and efficiency.

Key Characteristics:

  • Compose interfaces and implementations
  • Implement interface transparency
  • Improve code reusability

Common Scenarios:

javascript
// Adapter Pattern - Compatible with different data sources
class LegacyDataAPI {
  getUsers() {
    return [
      { name: "John Smith", age: 25 },
      { name: "Jane Doe", age: 30 },
    ];
  }
}

class NewDataAPI {
  fetchUsers() {
    return Promise.resolve([
      { username: "Bob Johnson", userAge: 28 },
      { username: "Alice Williams", userAge: 35 },
    ]);
  }
}

class LegacyAPIAdapter {
  constructor(legacyAPI) {
    this.legacyAPI = legacyAPI;
  }

  fetchUsers() {
    const users = this.legacyAPI.getUsers();
    return Promise.resolve(
      users.map((user) => ({
        username: user.name,
        userAge: user.age,
      }))
    );
  }
}

// Use adapter to unify interface
const legacyAPI = new LegacyDataAPI();
const adapter = new LegacyAPIAdapter(legacyAPI);

async function displayUsers() {
  const users = await adapter.fetchUsers();
  console.log("Unified format:", users);
  // Output: [{ username: 'John Smith', userAge: 25 }, { username: 'Jane Doe', userAge: 30 }]
}

Behavioral Patterns โ€‹

Behavioral patterns focus on responsibility distribution between objects, describing how objects communicate and collaborate.

Key Characteristics:

  • Focus on algorithm and responsibility distribution between objects
  • Describe communication patterns between objects
  • Improve system flexibility and extensibility

Common Scenarios:

javascript
// Command Pattern - Implement undo/redo functionality
class TextEditor {
  constructor() {
    this.content = "";
    this.history = [];
    this.currentIndex = -1;
  }

  executeCommand(command) {
    command.execute(this.content);
    this.content = command.getResult();

    // Remove history after current position
    this.history = this.history.slice(0, this.currentIndex + 1);
    this.history.push(command);
    this.currentIndex++;
  }

  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      const command = this.history[this.currentIndex];
      command.undo(this.content);
      this.content = command.getResult();
    }
  }
}

class InsertCommand {
  constructor(text, position) {
    this.text = text;
    this.position = position;
  }

  execute(currentContent) {
    return (
      currentContent.slice(0, this.position) +
      this.text +
      currentContent.slice(this.position)
    );
  }

  undo(currentContent) {
    return (
      currentContent.slice(0, this.position) +
      currentContent.slice(this.position + this.text.length)
    );
  }

  getResult() {
    return this.execute(this.previousContent);
  }
}

// Usage example
const editor = new TextEditor();
editor.executeCommand(new InsertCommand("Hello", 0));
editor.executeCommand(new InsertCommand(" World", 5));
editor.undo(); // Undo last operation

Design Patterns in Front-End Development โ€‹

1. JavaScript-Specific Patterns โ€‹

Due to JavaScript's dynamic nature and prototype chain characteristics, some design patterns have special implementations in JavaScript:

javascript
// Prototype Pattern - JavaScript's prototype chain
class Shape {
  constructor(type) {
    this.type = type;
  }

  clone() {
    return Object.create(this);
  }
}

const circlePrototype = new Shape("circle");
circlePrototype.radius = 5;
circlePrototype.area = function () {
  return Math.PI * this.radius * this.radius;
};

// Clone prototype object
const circle1 = circlePrototype.clone();
const circle2 = circlePrototype.clone();

console.log(circle1.area()); // 78.54
console.log(circle2.area()); // 78.54

2. React Component Patterns โ€‹

Common design patterns in React development include:

javascript
// Higher-Order Component Pattern - Reuse component logic
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const [user, setUser] = useState(null);

    useEffect(() => {
      checkAuthStatus().then(setUser);
    }, []);

    if (!user) {
      return <div>Please log in first</div>;
    }

    return <WrappedComponent {...props} user={user} />;
  };
}

// Use higher-order component
const UserProfile = withAuth(({ user }) => (
  <div>
    <h1>Welcome, {user.name}!</h1>
  </div>
));

// Custom Hook Pattern - Logic reuse
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Use custom hook
function UserComponent({ userId }) {
  const { data: user, loading, error } = useApi(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Loading failed: {error.message}</div>;
  if (!user) return <div>User not found</div>;

  return <div>{user.name}</div>;
}

3. Vue Component Patterns โ€‹

There are also corresponding design pattern implementations in Vue development:

javascript
// Mixin Pattern - Component functionality extension
const LoggerMixin = {
  created() {
    console.log(`Component ${this.$options.name} has been created`);
  },
  methods: {
    log(message) {
      console.log(`[${this.$options.name}] ${message}`);
    }
  }
};

export default {
  mixins: [LoggerMixin],
  created() {
    this.log('Component initialization complete');
  }
};

// Custom Directive Pattern
const VFocus = {
  inserted(el) {
    el.focus();
  }
};

// Register global directive
Vue.directive('focus', VFocus);

// Use in template
<template>
  <input v-focus placeholder="Auto-focus">
</template>

Design Pattern Application Principles โ€‹

1. Single Responsibility Principle โ€‹

Each class should only be responsible for one single responsibility, avoiding feature overloading:

javascript
// Bad example - Violates Single Responsibility Principle
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // Database operations
  save() {
    // Code to save to database
  }

  // Email sending
  sendEmail(content) {
    // Code to send email
  }

  // Format display
  getDisplayName() {
    return `${this.name} <${this.email}>`;
  }
}

// Good example - Follows Single Responsibility Principle
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  getDisplayName() {
    return `${this.name} <${this.email}>`;
  }
}

class UserRepository {
  save(user) {
    // Specialized for database operations
  }
}

class EmailService {
  sendEmail(user, content) {
    // Specialized for email sending
  }
}

2. Open-Closed Principle โ€‹

Software entities should be open for extension but closed for modification:

javascript
// Using Strategy Pattern to satisfy Open-Closed Principle
class PaymentStrategy {
  process(amount) {
    throw new Error("Subclass must implement this method");
  }
}

class PayPalStrategy extends PaymentStrategy {
  process(amount) {
    // PayPal implementation
    return { success: true, transactionId: "paypal_" + Date.now() };
  }
}

class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  process(amount) {
    return this.strategy.process(amount);
  }
}

// Adding new payment methods doesn't require modifying existing code
class StripeStrategy extends PaymentStrategy {
  process(amount) {
    // Stripe implementation
    return { success: true, transactionId: "stripe_" + Date.now() };
  }
}

3. Dependency Inversion Principle โ€‹

High-level modules should not depend on low-level modules; both should depend on abstractions:

javascript
// Application of Dependency Inversion Principle
class Database {
  save(data) {
    // Database save implementation
    console.log("Saving to database:", data);
  }
}

class FileStorage {
  save(data) {
    // File save implementation
    console.log("Saving to file:", data);
  }
}

class DataRepository {
  constructor(storage) {
    this.storage = storage; // Depend on abstraction, not concrete implementation
  }

  saveData(data) {
    this.storage.save(data);
  }
}

// Can inject different storage implementations
const dbRepo = new DataRepository(new Database());
const fileRepo = new DataRepository(new FileStorage());

dbRepo.saveData({ name: "User 1", age: 25 });
fileRepo.saveData({ name: "User 2", age: 30 });

Summary โ€‹

Design patterns are important tools in software engineering. They provide proven solutions to common design problems.

Key Points Recap:

  • Design patterns are typical solutions to software design problems
  • GoF's 23 patterns are divided into three categories: creational, structural, and behavioral
  • Design patterns improve code reusability, maintainability, and extensibility
  • Front-end development has many pattern implementations specific to JavaScript and frameworks
  • Applying design patterns requires following object-oriented design principles

Mastering design patterns will help you:

  • Write better code: Clear structure, easy to understand and maintain
  • Improve development efficiency: Reuse mature solutions, avoid reinventing the wheel
  • Enhance system architecture: Design flexible, extensible software systems
  • Improve code quality: Follow best practices, reduce technical debt

Design patterns are not a silver bullet and should be reasonably selected and applied based on specific scenarios. Overusing design patterns can actually increase code complexity. The key is to understand what problems each pattern solves and their applicable scenarios, and apply them flexibly in actual development.

Last updated: