1. Introduction

The use of design patterns to address different aspects of how we write code is one of the significant approaches to software development. Loose coupling is a principal focus of software design, and the chain of responsibility pattern addresses loose coupling head-on.

In this article, we’ll explore the intricacies of the chain of responsibility pattern using a practical example in Kotlin.

2. What Is the Chain of Responsibility Design Pattern?

The chain of responsibility design pattern is aimed at achieving loose coupling in software design. In this pattern, a client passes a request to a chain of objects or receivers for processing. Subsequently, the objects or handlers in the chain will have to decide for themselves what particular object will be responsible for handling the request and whether the request should be sent to the next object in the chain.

For instance, to elaborate on this design pattern, let’s imagine a scenario where someone calls the technical help desk with a query regarding a product. A technical desk correspondent tries to resolve this query but can’t, so they pass it on to the billing department, and a correspondent from there also attempts to resolve this query and fails, so they push it over to the customer satisfaction desk where another correspondent fails to resolve the issue.

Since the query still persists, we’ll have to move it to different correspondents until someone resolves the issue. In this case, we are the client or request object and the support correspondents form a chain of receivers or handlers.

The graph above depicts the support center system scenario explained above. Each handler has the option of handling the request from the client, in which case we’ll get a result. If a handler can’t process it, they pass it to the next handler to also attempt handling the request.

3. Demonstration

Now, let’s get to the coding part. We’ll demonstrate the chain of responsibility pattern by looking at our support center system from the previous section.

3.1. Participants

First, the participants in the chain of responsibility pattern are;

  • SupportCenterClient: the client that initiates a request for any handler to process
  • TechnicalSupportCenter: handles technical queries
  • BillsSupportCenter: handles issues related to billing or payment
  • CustomerSatisfactionSupportCenter: handles issues pertaining to client services and satisfaction

To better implement our support centers or handlers, we’ll create a class AbstractSupportCenterHandler that provides important methods and values to be implemented in each handler class:

abstract class AbstractSupportCenterHandler(private val requestType: RequestType) {
    enum class RequestType {
        TECHNICAL, BILL, CUSTOMER_SATISFACTION, UNKNOWN
    }

    open var nextHandler: AbstractSupportCenterHandler? = null

    open fun nextHandler(handler: AbstractSupportCenterHandler) {
        this.nextHandler = handler
    }

    open fun receiveRequest(requestType: RequestType, message: String): String {
        when (this.requestType == requestType) {
            true -> return handleRequest(message)
            else -> return nextHandler?.receiveRequest(requestType, message)
                ?: return "No next handler for this request"
        }
    }

    protected abstract fun handleRequest(message: String): String
}

The nextHandler() method stores the next handler to attempt to process a request, while the receiveRequest() method takes a requestType and message as a parameter. It uses the requestType parameter to determine the message request. The handleRequest() method must be implemented by a concrete class.

3.2. Handler Classes (Support Centers)

Subsequently, our handler classes will be concrete classes that extend the AbstractSupportCenterHandler class. For our example, we will have three handlers. First, let’s write the handler representing the technical support center:

class TechnicalSupportCenter(requestType: RequestType) : AbstractSupportCenterHandler(requestType) {
    override var nextHandler: AbstractSupportCenterHandler? =
        BillsSupportCenter(RequestType.BILL)
    override fun handleRequest(message: String): String {
        return "Handler: TechnicalSupportHandler - request: $message"
    }
}

Now, let’s write the handler representing the billing support center:

class BillsSupportCenter(requestType: RequestType) : AbstractSupportCenterHandler(requestType){
    override var nextHandler: AbstractSupportCenterHandler =
        CustomerSatisfactionSupportCenter(RequestType.CUSTOMER_SATISFACTION)
    override fun handleRequest(message: String): String {
        return "Handler: BillsSupportHandler - request: $message"
    }
}

And finally, let’s write a third handler to represent the customer satisfaction support center:

class CustomerSatisfactionSupportCenter(
    requestType: RequestType
) : AbstractSupportCenterHandler(requestType) {
    override var nextHandler: AbstractSupportCenterHandler? = null
    override fun handleRequest(message: String): String {
        return "Handler: CustomerSatisfactionSupportHandler - request: $message"
    }
}

Each handler provides a concrete implementation for the handleRequest() method and specifies its name and the request message it is handling.

Now, we need our client object — the SupportCenterClient class. This class will be able to make requests to our support center system and provides a way to create the initial handler object:

object SupportCenterClient {
    val technicalHandler =
        TechnicalSupportCenter(AbstractSupportCenterHandler.RequestType.TECHNICAL)
}

This class contains a default construction and initialization of all concrete handler classes. It also constructs the chain of handlers via the nextHandler() method.

4. Testing

Finally, let’s test the support center system. First, we’re going to look at a test case where each handler handles appropriate issues:

@Test
fun `support center attempt to process technical request`(){
    val result1 = SupportCenterClient.technicalHandler
        .receiveRequest(AbstractSupportCenterHandler.RequestType.TECHNICAL, "technical issue.")
    assertEquals("Handler: TechnicalSupportHandler - request: technical issue.", result1)
}

However, from our design, the TechnicalSupportCenter shouldn’t be able to treat billing issues. Only the billing team should be able to do that:

@Test
fun `support center attempt to process billing request`(){
    val result = SupportCenterClient.technicalHandler
        .receiveRequest(AbstractSupportCenterHandler.RequestType.BILL, "billing issue.")
    assertEquals("Handler: BillsSupportHandler - request: billing issue.", result)
}

So, since the TechnicalSupportCenter can’t handle billing requests, the next handler in the chain is the BillsSupportCenter.

Another interesting scenario we should test is to make sure that no other department can handle customer satisfaction issues except the CustomerSatisfactionSupportHandler:

@Test
fun `support center attempt to process customer happiness request`(){
    val result = SupportCenterClient.technicalHandler
        .receiveRequest(
            AbstractSupportCenterHandler.RequestType.CUSTOMER_SATISFACTION,
            "customer satisfaction issue."
        )
    assertEquals(
        "Handler: CustomerSatisfactionSupportHandler - request: customer satisfaction issue.",
        result
    )
}

Next, it is possible that our request can’t be handled by any handler based on our chain of handlers. In this test, a TechnicalSupportCenter tries to treat an unknown request but it can’t, and neither can any of the others in our chain of handlers:

@Test
fun `no center can process client request`(){
    val result = SupportCenterClient.technicalHandler
        .receiveRequest(AbstractSupportCenterHandler.RequestType.UNKNOWN, "Other issue.")
    assertEquals("No next handler for this request", result)
}

5. Conclusion

In this tutorial, we’ve explored what the chain of responsibility pattern is all about and what design issues it addresses. We equally show using code how to realize this pattern, what the various participants are, and what their roles are. Ultimately, we tested our system according to various possible scenarios.

As always, the code samples and relevant test cases pertaining to this article can be found over on GitHub.