The chain of responsibility design pattern

Nowadays, with the growth of data sizes and the hype around Big Data, stream processing is something that many applications will have to be able to do. Stream processing is characterized by an endless stream of data, which is passed from one object to another while each of them could be doing some processing and then passing it on to the next one. In other cases, data could be moved on in the chain until it arrives at an object which knows how to process a certain command.

The preceding behavior is really suitable for the chain of responsibility design pattern. The purpose of chain of responsibility is to:

Note

Decouple the sender of a request from its receiver by giving multiple objects the chance to handle the request.

There could be some variations to the chain of responsibility design pattern. The original pattern is that whenever a request reaches an object that can process it, it doesn't go any further. However, in some cases, we might need to push the request further or even multiply it and broadcast to other receivers.

It is worth noting that the chain of responsibility is not data specific at all and it can be used in any scenario where the preceding characteristics emerge.

Class diagram

A common example used to illustrate the chain of responsibility design pattern is about event handling in applications, depending on whether they come from a mouse or a keyboard action. For our class diagram and code example, let's take a look at something else that we use every day—ATMs. How do they return the right amount in different note combinations? The answer is, of course, chain of responsibility.

We will present two diagrams here—one of the classes that allows us to achieve the chain of responsibility pattern and another that will show how those classes are used together to build our ATM.

First let's take a look at our classes separately:

Class diagram

In the preceding diagram, we have a base class (represented as trait in Scala), which is then extended by the different concrete dispensers. Each dispenser has an optional instance of the same class, and in this way we can build a chain. The dispense method is the same for all the dispensers and then each of the dispensers has a different amount and a different next element in the chain.

Things will get much clearer as we present our ATM implementation. It can be seen in the following figure:

Class diagram

The preceding figure shows the actual chain that we have in our ATM. Whenever someone requests money, the ATM will go to the dispenser for 50 pound notes, then to a lower dispenser and so on until the request of the user is satisfied. In the next subsection, we will present our code step by step.

Code example

Let's look at the code for the preceding example, one step at a time. First of all, we have defined a Money class that represents the amount the user requests. The definition looks like the following:

case class Money(amount: Int)

Let's now have a look at the Dispenser trait. It is the one that the concrete dispensers extend, as shown here:

trait Dispenser {
  val amount: Int
  val next: Option[Dispenser]
  
  def dispense(money: Money): Unit = {
    if (money.amount >= amount) {
      val notes = money.amount / amount
      val left = money.amount % amount
      System.out.println(s"Dispensing $notes note/s of $amount.")
      if (left > 0) next.map(_.dispense(Money(left)))
    } else {
      next.foreach(_.dispense(money))
    }
  }
}

As we mentioned in the preceding section, the dispense method is the same for everyone who extends our Dispenser, but the amount and the next element of the chain is to be defined by whoever extends it. The dispense method tries to return as many notes as possible of the given nomination; after this, it passes the responsibility to the next dispenser if there is still money to be given.

The following code block shows our implementations of the different dispensers—for notes of 50, 20, 10, and 5 pounds:

class Dispenser50(val next: Option[Dispenser]) extends Dispenser {
  override val amount = 50
}

class Dispenser20(val next: Option[Dispenser]) extends Dispenser {
  override val amount: Int = 20
}

class Dispenser10(val next: Option[Dispenser]) extends Dispenser {
  override val amount: Int = 10
}

class Dispenser5(val next: Option[Dispenser]) extends Dispenser {
  override val amount: Int = 5
}

What we have shown so far is the essence of the chain of responsibility design pattern. Using the defined classes, we will now build a chain that can be used easily.

. Here is the code for our ATM class:

class ATM {
  val dispenser: Dispenser = {
    val d1 = new Dispenser5(None)
    val d2 = new Dispenser10(Some(d1))
    val d3 = new Dispenser20(Some(d2))
    new Dispenser50(Some(d3))
  }
  
  def requestMoney(money: Money): Unit = {
    if (money.amount % 5 != 0) {
      System.err.println("The smallest nominal is 5 and we cannot satisfy your request.")
    } else {
      dispenser.dispense(money)
    }
  }
}

In the preceding code, we built the dispenser chain that will be used by our ATM class. The order here is really important for the correct functioning of the system. We have also done some sanity checks. The use of our ATM class is then pretty straightforward with the following application:

object ATMExample {
  def main(args: Array[String]): Unit = {
    val atm = new ATM
    printHelp()
    Source.stdin.getLines().foreach {
      case line =>
        processLine(line, atm)
    }
  }
  
  def printHelp(): Unit = {
    System.out.println("Usage: ")
    System.out.println("1. Write an amount to withdraw...")
    System.out.println("2. Write EXIT to quit the application.")
  }
  
  def processLine(line: String, atm: ATM): Unit = {
    line match {
      case "EXIT" =>
        System.out.println("Bye!")
        System.exit(0)
      case l =>
        try {
          atm.requestMoney(Money(l.toInt))
          System.out.println("Thanks!")
        } catch {
          case _: Throwable =>
            System.err.println(s"Invalid input: $l.")
            printHelp()
        }
      
    }
  }
}

This is an interactive application that waits for the user input and then uses the ATM. Let's see how an example run of this will look in the following screenshot:

Code example

As you can see in the code, our ATM doesn't have the extra functionality that other ATMs have—check note availability. This, however, is a functionality that can be extended further.

The chain of responsibility design pattern the Scala way

Looking more carefully at the code and the class diagram, you can see some similarities to the decorator design pattern. This means that here we can use the same stackable traits, which use the abstract override construct. We've already seen an example of this and it will not provide you with any new information. However, there is another functionality of the Scala programming language that we can use in order to achieve the chain of responsibility—partial functions.

Using partial functions, we don't need to define the specific dispenser classes separately. Our dispenser will change to the following:

trait PartialFunctionDispenser {
  
  def dispense(dispenserAmount: Int): PartialFunction[Money, Money] = {
    case Money(amount) if amount >= dispenserAmount =>
      val notes = amount / dispenserAmount
      val left = amount % dispenserAmount
      System.out.println(s"Dispensing $notes note/s of $dispenserAmount.")
      Money(left)
    case m @ Money(amount) => m
  }
}

Of course, there are different ways to do this—we can have an abstract trait and then implement the partial function (something similar to the original example) and not specify the dispenserAmount parameter, or we can have one trait with different implementations of this function instead of passing the dispenserAmount parameter, and so on. Doing this, however, allows us to later simulate the existence of an infinite number of different bills.

After we have our new dispenser, which returns a PartialFunction instead of nothing (Unit), we can define our ATM class:

class PartialFunctionATM extends PartialFunctionDispenser {
  
  val dispenser = dispense(50).andThen(dispense(20)).andThen(dispense(10)).andThen(dispense(5)) 
  
  def requestMoney(money: Money): Unit = {
    if (money.amount % 5 != 0) {
      System.err.println("The smallest nominal is 5 and we cannot satisfy your request.")
    } else {
      dispenser(money)
    }
  }
}

The interesting part here is the dispenser field and the way we use it. In the preceding code, we chained multiple partial functions using the andThen method and finally, we used the result of them as a method.

Note

Depending on what chains the developer wants to create, they can use the orElse or andThen methods of the partial functions. The former is useful for single handlers and the latter for chaining.

Running the original example, but with substituted ATM implementations, will yield absolutely identical results.

As you saw in this subsection, using partial functions can make our application more flexible and will require us to write less code. However, it might be more demanding in terms of understanding advanced Scala language concepts.

Just for completeness, it is worth mentioning that we can also implement the chain of responsibility design pattern using the Akka library. We will be looking into this library in the later chapters of this book, and you will hopefully be able to see how this design pattern can be moved to reactive programming with Scala.

What is it good for?

The chain of responsibility design pattern should be used when we want to decouple a sender of a request from the receivers and have these receivers separated into their own entities. It is good for creating pipelines and handling events.

What is it not so good for?

As a negative and a possible pitfall of the chain of responsibility design pattern, we will talk about the implementation involving partial functions. This is because it might not always be able to achieve what the developers want, and this could further complicate code and affect readability.

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

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