阿里云主机折上折
  • 微信号
Current Site:Index > The request processing flow of the Chain of Responsibility pattern.

The request processing flow of the Chain of Responsibility pattern.

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

Basic Concepts of the Chain of Responsibility Pattern

The Chain of Responsibility pattern is a behavioral design pattern that allows you to pass requests along a chain of handlers. Upon receiving a request, each handler can either process the request or pass it to the next handler in the chain. This pattern decouples the sender and receiver of the request, giving multiple objects the opportunity to handle the request.

In JavaScript, the Chain of Responsibility pattern is typically represented as an object containing a reference to another object, forming a chain. When a client initiates a request, the request travels along this chain until an object handles it.

Structure of the Chain of Responsibility Pattern

A typical Chain of Responsibility pattern consists of the following key components:

  1. Handler (Abstract Handler): Defines the interface for handling requests, usually including a method to handle requests and a method to set the successor.
  2. ConcreteHandler (Concrete Handler): Implements the abstract handler's interface, handles the requests it is responsible for, and can access its successor.
  3. Client: Creates the handler chain and submits requests to the concrete handler objects in the chain.
// Abstract Handler
class Handler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler; // Facilitates chaining
  }

  handle(request) {
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }
}

// Concrete Handler A
class ConcreteHandlerA extends Handler {
  handle(request) {
    if (request === 'A') {
      return `HandlerA processed the request: ${request}`;
    }
    return super.handle(request);
  }
}

// Concrete Handler B
class ConcreteHandlerB extends Handler {
  handle(request) {
    if (request === 'B') {
      return `HandlerB processed the request: ${request}`;
    }
    return super.handle(request);
  }
}

// Usage Example
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();

handlerA.setNext(handlerB);

console.log(handlerA.handle('A')); // HandlerA processed the request: A
console.log(handlerA.handle('B')); // HandlerB processed the request: B
console.log(handlerA.handle('C')); // null

Implementation Methods of the Chain of Responsibility Pattern

In JavaScript, the Chain of Responsibility pattern can be implemented in several ways. Below are some common implementation methods:

1. Classic Implementation

As shown in the example above, a chain structure is formed by setting successors. This is the implementation closest to traditional object-oriented languages.

2. Implementing the Chain of Responsibility Using an Array

class HandlerChain {
  constructor() {
    this.handlers = [];
  }

  addHandler(handler) {
    this.handlers.push(handler);
    return this; // Supports chaining
  }

  handle(request) {
    for (const handler of this.handlers) {
      const result = handler.handle(request);
      if (result !== null) {
        return result;
      }
    }
    return null;
  }
}

// Concrete Handler
class DiscountHandler {
  handle(amount) {
    if (amount >= 1000) {
      return amount * 0.9; // 10% discount
    }
    return null;
  }
}

class ShippingHandler {
  handle(amount) {
    if (amount < 500) {
      return amount + 50; // Add shipping fee
    }
    return null;
  }
}

// Usage Example
const chain = new HandlerChain();
chain.addHandler(new DiscountHandler()).addHandler(new ShippingHandler());

console.log(chain.handle(800));  // 800 (no discount, free shipping)
console.log(chain.handle(1200)); // 1080 (10% discount)
console.log(chain.handle(300));  // 350 (shipping fee added)

3. Implementing the Chain of Responsibility Using Functions

JavaScript functions are first-class citizens, allowing for a more concise implementation of the Chain of Responsibility:

function createHandlerChain(...handlers) {
  return function(request) {
    for (const handler of handlers) {
      const result = handler(request);
      if (result !== null) {
        return result;
      }
    }
    return null;
  };
}

// Handler Functions
function managerHandler(request) {
  if (request.amount <= 1000) {
    return `Manager approved the purchase of ${request.amount} yuan`;
  }
  return null;
}

function directorHandler(request) {
  if (request.amount <= 5000) {
    return `Director approved the purchase of ${request.amount} yuan`;
  }
  return null;
}

function ceoHandler(request) {
  if (request.amount <= 10000) {
    return `CEO approved the purchase of ${request.amount} yuan`;
  }
  return null;
}

// Create the Chain of Responsibility
const approvalChain = createHandlerChain(managerHandler, directorHandler, ceoHandler);

// Usage Example
console.log(approvalChain({ amount: 800 }));   // Manager approved the purchase of 800 yuan
console.log(approvalChain({ amount: 3000 }));  // Director approved the purchase of 3000 yuan
console.log(approvalChain({ amount: 8000 }));  // CEO approved the purchase of 8000 yuan
console.log(approvalChain({ amount: 20000 })); // null

Application Scenarios of the Chain of Responsibility Pattern in Frontend Development

The Chain of Responsibility pattern has many practical applications in frontend development. Below are a few typical examples:

1. Event Bubbling Mechanism

The DOM event bubbling mechanism is itself an implementation of the Chain of Responsibility pattern. Events start from the most specific element and propagate up to less specific nodes.

document.getElementById('child').addEventListener('click', function(e) {
  console.log('Child clicked');
  // e.stopPropagation(); // Stop further propagation
});

document.getElementById('parent').addEventListener('click', function() {
  console.log('Parent clicked');
});

document.body.addEventListener('click', function() {
  console.log('Body clicked');
});

2. Middleware Mechanism

The middleware mechanism in frameworks like Express/Koa is a classic application of the Chain of Responsibility pattern:

const Koa = require('koa');
const app = new Koa();

// Middleware 1
app.use(async (ctx, next) => {
  console.log('Middleware 1 - start');
  await next();
  console.log('Middleware 1 - end');
});

// Middleware 2
app.use(async (ctx, next) => {
  console.log('Middleware 2 - start');
  await next();
  console.log('Middleware 2 - end');
});

// Route Handler
app.use(async ctx => {
  console.log('Route handler');
  ctx.body = 'Hello World';
});

app.listen(3000);

3. Form Validation

The Chain of Responsibility pattern is well-suited for handling complex form validation logic:

class Validator {
  constructor() {
    this.nextValidator = null;
  }

  setNext(validator) {
    this.nextValidator = validator;
    return validator;
  }

  validate(input) {
    if (this.nextValidator) {
      return this.nextValidator.validate(input);
    }
    return { isValid: true, message: '' };
  }
}

class RequiredValidator extends Validator {
  validate(input) {
    if (!input.value) {
      return { isValid: false, message: `${input.name} is required` };
    }
    return super.validate(input);
  }
}

class EmailValidator extends Validator {
  validate(input) {
    if (input.type === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.value)) {
      return { isValid: false, message: 'Please enter a valid email address' };
    }
    return super.validate(input);
  }
}

class LengthValidator extends Validator {
  constructor(min, max) {
    super();
    this.min = min;
    this.max = max;
  }

  validate(input) {
    if (input.value && (input.value.length < this.min || input.value.length > this.max)) {
      return { 
        isValid: false, 
        message: `${input.name} length must be between ${this.min}-${this.max} characters`
      };
    }
    return super.validate(input);
  }
}

// Usage Example
const requiredValidator = new RequiredValidator();
const emailValidator = new EmailValidator();
const lengthValidator = new LengthValidator(6, 20);

requiredValidator.setNext(emailValidator).setNext(lengthValidator);

const formInput = {
  name: 'Password',
  type: 'password',
  value: '123'
};

const result = requiredValidator.validate(formInput);
console.log(result); // { isValid: false, message: 'Password length must be between 6-20 characters' }

Variations and Extensions of the Chain of Responsibility Pattern

1. Asynchronous Chain of Responsibility

In real-world development, handlers may need to perform asynchronous operations. We can implement an asynchronous Chain of Responsibility using Promises:

class AsyncHandler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  async handle(request) {
    if (this.nextHandler) {
      return await this.nextHandler.handle(request);
    }
    return null;
  }
}

class AuthHandler extends AsyncHandler {
  async handle(request) {
    console.log('AuthHandler processing...');
    // Simulate async operation
    await new Promise(resolve => setTimeout(resolve, 1000));
    if (!request.token) {
      throw new Error('Unauthorized');
    }
    return super.handle(request);
  }
}

class LoggingHandler extends AsyncHandler {
  async handle(request) {
    console.log('LoggingHandler processing...');
    await new Promise(resolve => setTimeout(resolve, 500));
    console.log(`Request log: ${JSON.stringify(request)}`);
    return super.handle(request);
  }
}

class BusinessHandler extends AsyncHandler {
  async handle(request) {
    console.log('BusinessHandler processing...');
    await new Promise(resolve => setTimeout(resolve, 800));
    return `Processing result: ${request.data}`;
  }
}

// Usage Example
(async () => {
  const authHandler = new AuthHandler();
  const loggingHandler = new LoggingHandler();
  const businessHandler = new BusinessHandler();

  authHandler.setNext(loggingHandler).setNext(businessHandler);

  try {
    const result = await authHandler.handle({
      token: 'abc123',
      data: 'Important business data'
    });
    console.log(result); // Processing result: Important business data
  } catch (error) {
    console.error('Error:', error.message);
  }
})();

2. Interruptible Chain of Responsibility

Sometimes we need to interrupt the Chain of Responsibility under certain conditions:

class InterruptibleHandler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    const result = this.process(request);
    if (result.shouldStop) {
      return result.value;
    }
    if (this.nextHandler) {
      return this.nextHandler.handle(request);
    }
    return null;
  }

  process(request) {
    // Default implementation, can be overridden by subclasses
    return { shouldStop: false, value: null };
  }
}

class CacheHandler extends InterruptibleHandler {
  constructor(cache) {
    super();
    this.cache = cache || {};
  }

  process(request) {
    if (this.cache[request.key]) {
      return {
        shouldStop: true,
        value: `Retrieved from cache: ${this.cache[request.key]}`
      };
    }
    return { shouldStop: false, value: null };
  }
}

class DataHandler extends InterruptibleHandler {
  process(request) {
    // Simulate data processing
    const result = `Processed data: ${request.key.toUpperCase()}`;
    return {
      shouldStop: true, // Interrupt the chain after processing
      value: result
    };
  }
}

// Usage Example
const cache = { foo: 'Cached value' };
const cacheHandler = new CacheHandler(cache);
const dataHandler = new DataHandler();

cacheHandler.setNext(dataHandler);

console.log(cacheHandler.handle({ key: 'foo' })); // Retrieved from cache: Cached value
console.log(cacheHandler.handle({ key: 'bar' })); // Processed data: BAR

3. Multi-Functional Chain of Responsibility

Handlers in the Chain of Responsibility can not only process requests but also modify requests or responses:

class TransformHandler {
  constructor() {
    this.nextHandler = null;
  }

  setNext(handler) {
    this.nextHandler = handler;
    return handler;
  }

  handle(request) {
    // Pre-process the request
    const processedRequest = this.preProcess(request);
    
    // Pass to the next handler
    let response;
    if (this.nextHandler) {
      response = this.nextHandler.handle(processedRequest);
    } else {
      response = { status: 'default' };
    }
    
    // Post-process the response
    return this.postProcess(response);
  }

  preProcess(request) {
    // Default no processing, can be overridden by subclasses
    return request;
  }

  postProcess(response) {
    // Default no processing, can be overridden by subclasses
    return response;
  }
}

class RequestLogger extends TransformHandler {
  preProcess(request) {
    console.log('Received request:', request);
    return {
      ...request,
      timestamp: Date.now()
    };
  }

  postProcess(response) {
    console.log('Returning response:', response);
    return response;
  }
}

class AuthTransformer extends TransformHandler {
  preProcess(request) {
    if (!request.token) {
      throw new Error('Token missing');
    }
    return {
      ...request,
      userId: this.extractUserId(request.token)
    };
  }

  extractUserId(token) {
    // Simulate extracting user ID from token
    return token.split('-')[0];
  }
}

class BusinessLogic extends TransformHandler {
  handle(request) {
    // Do not call nextHandler, acts as the end of the chain
    return {
      status: 'success',
      data: `Processed request for user ${request.userId}: ${request.action}`
    };
  }
}

// Usage Example
const logger = new RequestLogger();
const auth = new AuthTransformer();
const business = new BusinessLogic();

logger.setNext(auth).setNext(business);

try {
  const response = logger.handle({
    token: '12345-abcde',
    action: 'updateProfile'
  });
  console.log('Final response:', response);
} catch (error) {
  console.error('Processing failed:', error.message);
}

Pros and Cons of the Chain of Responsibility Pattern

Pros

  1. Reduced Coupling: The request sender does not need to know which object will handle its request, and the receiver does not need to know the full context of the request.
  2. Dynamic Composition: Handlers can be added or modified dynamically, making it easy to add new handler classes.
  3. Single Responsibility: Each handler class only needs to focus on its own responsibilities, adhering to the Single Responsibility Principle.
  4. Flexibility: The processing order can be adjusted flexibly, or certain steps can be skipped.

Cons

  1. Performance Considerations: Requests may traverse the entire chain before being processed, potentially affecting performance in the worst case.
  2. Debugging Difficulty: The implicit passing of requests can make it difficult to track the request processing flow during debugging.
  3. Guaranteed Processing: There is no guarantee that a request will be processed, and additional logic may be needed to handle unprocessed requests.
  4. Chain Maintenance: Improper chain construction can lead to circular references or incorrect processing order.

Relationship Between the Chain of Responsibility Pattern and Other Patterns

Relationship with the Decorator Pattern

Both the Chain of Responsibility and Decorator patterns are based on recursive composition, but their purposes differ:

  • The Decorator pattern dynamically adds responsibilities to objects, and all decorators are executed.
  • The Chain of Responsibility pattern allows requests to be handled by one or more handlers, and processing may stop at any point in the chain.

Relationship with the Command Pattern

The Chain of Responsibility pattern is often used with the Command pattern:

  • The Command pattern encapsulates requests as objects.
  • The Chain of Responsibility pattern determines which object will handle the command object.

Relationship with the Composite Pattern

The structure of the Composite pattern is similar to the Chain of Responsibility pattern, but their purposes differ:

  • The Composite pattern represents part-whole hierarchies.
  • The Chain of Responsibility pattern is used for passing and handling requests.

Best Practices in Real-World Projects

1. Control the Chain Length Appropriately

An excessively long chain can impact performance and increase debugging difficulty. In practice, it is recommended to:

  • Keep the chain length within a reasonable range (typically no more than 10 handlers).
  • Consider merging some handlers for complex logic.
  • Use the Composite pattern to group related handlers into sub-chains.

2. Define Clear Handling Result Conventions

Ensure all handlers adhere to consistent conventions for handling results, such as:

  • Returning null or undefined to indicate unhandled requests.
  • Returning specific values to indicate handled requests.
  • Throwing exceptions to indicate processing failures.

3. Provide Debugging Support

To facilitate debugging:

  • Add logging functionality to the handler chain.
  • Implement visualization tools to show the flow of requests through the chain.
  • Provide performance monitoring to identify bottleneck handlers.
class DebuggableHandler extends Handler {
  constructor(name) {
    super();
    this.name = name;
  }

  handle(request) {
    console.log(`[${this.name}] Handling request:`, request);
    const start = performance.now();
    
    const result = super.handle(request);
    
    const duration = performance.now() - start;
    console.log(`[${this.name}] Processing completed, duration: ${duration.toFixed(2)}ms`);
    
    return result;
  }
}

4. Consider Security Factors

In security-related handler chains:

  • Ensure critical handlers cannot be skipped.
  • Validate the integrity of the handler chain.
  • Consider using immutable request objects to prevent intermediate modifications.

5. Integration with Promise Chains

In modern JavaScript, the Chain of Responsibility can be combined with Promise chains:

function createPromiseChain(...handlers) {
  return function(input) {
    return handlers.reduce((promise, handler) => {
      return promise.then(result => {
        if (result.handled) {
          return result;
        }
        return handler(input);
      });
    }, Promise.resolve({ handled: false }));
  };
}

// Handler Functions
function checkAuth(input) {
  return new Promise(resolve => {
    setTimeout(() => {
      if (!input.token) {
        resolve({ handled: true, error:

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱: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 ☕.