The chain-of-responsibility pattern

The chain-of-responsibility (CoR) pattern is used to process the request using a chain of request handlers, whereas each handler has its own distinct and independent responsibility.

This pattern is quite common in many applications. For example, web servers usually handle HTTP requests using so-called middleware. Each piece of middleware is responsible for performing a specific task—for example, authenticating requests, maintaining cookies, validating requests, and performing business logic. A specific requirement about the CoR pattern is that any part of the chain can be broken at any time, resulting in an early exit of the process. In the preceding web server example, the authentication middleware may have decided that the user has not been authenticated, and that therefore, the user should be redirected to a separate website for login. This means that the rest of the middleware is skipped unless the user gets past the authentication step.

How do we design something like this in Julia? Let's look at a simple example:

mutable struct DepositRequest
id::Int
amount::Float64
end

A DepositRequest object contains an amount that a customer wants to deposit in their account. Our marketing department wants us to provide a thank-you note to the customer if the deposit amount is greater than $100,000. To process such a request, we have designed three functions, as follows:

@enum Status CONTINUE HANDLED

function update_account_handler(req::DepositRequest)
println("Deposited $(req.amount) to account $(req.id)")
return CONTINUE
end

function send_gift_handler(req::DepositRequest)
req.amount > 100_000 &&
println("=> Thank you for your business")
return CONTINUE
end

function notify_customer(req::DepositRequest)
println("deposit is finished")
return HANDLED
end

What is the responsibility of these functions?

  • The update_account_handler function is responsible for updating the account with the new deposit.
  • The send_gift_handler function is responsible for sending a thank-you note to the customer for a large deposit amount.
  • The notify_customer function is responsible for informing the customer after the deposit is made.

These functions also return an enum value, either CONTINUE or HANDLED, to indicate whether the request should be passed on to the next handler when the current one is finished.

It should be quite clear that these functions run in a specific order. In particular, the notify_customer function should run at the end of the transaction. For that reason, we can establish an array of functions:

handlers = [
update_account_handler,
send_gift_handler,
notify_customer
]

We can also have a function to execute these handlers in order:

function apply(req::DepositRequest, handlers::AbstractVector{Function})
for f in handlers
status = f(req)
status == HANDLED && return nothing
end
end

As part of this design, the loop will end immediately if any handler returns a value of HANDLED. Our test code for testing the function of sending the thank-you note to a premier customer is shown as follows:

function test()
println("Test: customer depositing a lot of money")
amount = 300_000
apply(DepositRequest(1, amount), handlers)

println(" Test: regular customer")
amount = 1000
apply(DepositRequest(2, amount), handlers)
end

Running the test gives us this result:

I will leave it as an exercise for you to build another function in this chain to perform an early exit. But for now, let's move on to the next pattern—the mediator pattern.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.143.239