Behavioral design mode - Chain of Responsibility

Posted by mtb211 on Mon, 31 Jan 2022 08:02:13 +0100

1. Introduction

I often hear my colleagues say that they have used the responsibility chain model in the project. Today, let's learn what the responsibility chain model is.

Chain of Responsibility is a kind of design pattern, which belongs to behavioral design pattern.

As the name suggests, the responsibility chain pattern creates a chain for requests that are processed on the chain. Usually, if a processor cannot process the request, it will pass the same request to the next processor in the chain.

2. Usage scenario

If a request needs to go through multiple processing steps, and multiple processing steps are abstracted into an execution chain, the responsibility chain pattern can be used. The usage scenarios of responsibility chain generally include:
(1) Submit a request to multiple processors, and only one processor will process the request in the final run time;
(2) Submit a request to multiple processors, and all processors will process the request;

3. Examples

In the actual scenario, financial approval is a responsibility chain mode. Assuming that an employee needs to reimburse an expense, the reviewers can be divided into:

  • Manager: you can only approve reimbursement of less than 1000 yuan;
  • Director: you can only approve reimbursement of less than 10000 yuan;
  • CEO: any quota can be approved.

When using the responsibility chain model to realize this reimbursement process, each reviewer only cares about the request within his own responsibility and handles it. Those beyond the scope of their responsibility will be thrown to the next reviewer for processing, so that when adding reviewers in the future, there is no need to change the existing logic.

We take C + + as an example to realize the responsibility chain mode.

First, we define a request object passed on the responsibility chain:

class Request {
	string name;
	double amount;

public:
	Request(string name, double amount) {
		this->name = name;
		this->amount = amount;
	}

	string getName() const {
		return name;
	}

	double getAmount() const {
		return amount;
	}
};

Secondly, we define a processor abstract class in both:

class Handler {
public:
	// Return 1 success
	// Return 2 reject
	// Return 0 to the next processing
	virtual int process(const Request& req) = 0;

	// Returns the name of the handler
	virtual string name() = 0;
};

Then, implement ManagerHandler, DirectorHandler and CEOHandler in turn.

// Manager review
class ManagerHandler : public Handler {
public:
	int process(const Request& req) {
		// If more than 1000 yuan can't be handled, hand it over to the next one
		if (req.getAmount() > 1000) {
			return 0;
		}
		// Biased against Bob
		return req.getName() == "bob" ? 2 : 1;
	}

	string name() {
		return "manager";
	}
};

// Director approval
class DirectorHandler: public Handler {
public:
	int process(const Request& req) {
		// If more than 10000 yuan can't be handled, hand it over to the next one
		if (req.getAmount() > 10000) {
			return 0;
		}
		return 1;
	}

	string name() {
		return "director";
	}
};

// President approval
class CEOHandler : public Handler {
public:
	int process(const Request& req) {
		return 1;
	}

	string name() {
		return "ceo";
	}
};

After having different handlers, we also need to combine these handlers into a chain and handle them through a unified entry:

class HandlerChain {
	// Hold all handlers
	list<Handler*> handlers;

public:
	void add(Handler* h) {
		handlers.push_back(h);
	}

	int process(const Request& req) {
		// Call each Handler in turn
		for (auto h : handlers) {
			int r = h->process(req);
			if (r != 0) {
				// If 1 or 2 is returned, the processing ends
				cout << req.getName() + " " + (r == 1 ? "approved by " : "denied by ") + h->name() << endl;
				return r;
			}
		}
		cout << "process failed" << endl;
		return -1;
	}
};

Now, we can assemble the responsibility chain, and then use the responsibility chain to process the request:

int main()
{
	HandlerChain chain;
	chain.add(new ManagerHandler());
	chain.add(new DirectorHandler());
	chain.add(new CEOHandler());

	chain.process(Request("bob", 100));
	chain.process(Request("tom", 1000));
	chain.process(Request("alice", 10000));
	chain.process(Request("thomas", 100000));

	return 0;
}

Run output:

bob denied by manager
tom approved by manager
alice approved by director
thomas approved by ceo

The responsibility chain mode itself is easy to understand. It should be noted that the order in which handlers are added is very important. If the order is wrong, the processing results may not meet the requirements.

4. Variants

In addition, there are many variants of the responsibility chain model. Some responsibility chains are implemented by manually calling the next Handler through a Handler to pass the Request, for example:

class AHandler: public Handler {
	Handler next;
public:
    void process(const Request& req) {
        if (!canProcess(req)) {
            // Hand it over to the next Handler
            next.process(req);
        } else {
            ...
        }
    }
};

There are also some responsibility chain modes. Each Handler has the opportunity to process requests. Usually, this responsibility chain is called Interceptor or Filter. Its purpose is not to find a Handler to process requests, but to do some work for each Handler, such as:

  • Record log;
  • Inspection authority;
  • Monitoring and reporting;
  • Distributed tracking;
  • ...

The key point to understand the principle of interceptor is to understand the trigger timing and sequence of interceptor.

Trigger timing:
The interceptor can intercept the request and response of the interface and process the request, response and context (described in popular language, that is, it can do something before the request is accepted and do something after the request is processed). Therefore, the interceptor is functionally divided into two parts: pre (before business logic processing) and post (after business logic processing).

Sequencing:
The interceptor has a clear sequence. The logic of the front part is executed in turn according to the registration order of the interceptor, and the rear part of the interceptor is executed in reverse order. As shown in the figure below:

For example, the Filter defined in the Servlet specification of Java EE is a responsibility chain mode, which not only allows each Filter to have the opportunity to process requests, but also allows each Filter to decide whether to "release" the request to the next Filter:

public class AuditFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        log(req);
        if (check(req)) {
            // Release:
            chain.doFilter(req, resp);
        } else {
            // Reject:
            sendError(resp);
        }
    }
}

This mode not only allows a Filter to process ServletRequest and ServletResponse independently, but also can "forge" ServletRequest and ServletResponse so that the next Filter can process and realize very complex functions.

reference

[1] Liao Xuefeng Chain of responsibility
[2] Cheng Jie Big talk design pattern C24: responsibility chain mode P245-256

Topics: Design Pattern