laitimes

Kotlin Design Pattern: Chain of Responsibility Pattern

author:Not bald programmer
Kotlin Design Pattern: Chain of Responsibility Pattern

The purpose of the Chain of Responsibilities model

The Chain of Responsibility pattern is designed to create a series of sequential processors. Each handler in the chain can terminate the chain continuation at any point, which means that only some of the handlers in the chain need to process data. In addition, the order of the chain can be freely composed according to the requirements.

What are the benefits to be gained from it?

  • Single responsibility: Each processor is responsible for one thing.
  • Controlled processing sequence.
  • Open-closed principle: New handlers can be added without breaking existing code.
  • Handlers can process data or pass it to the next handler based on runtime conditions.

implement

Kotlin Design Pattern: Chain of Responsibility Pattern

Abstract classes may be used when implementing this pattern. But if possible, it's better to use an interface. You may also encounter implementations that don't have the setNext(handler: Handler) method, but instead pass the Handler implementation through the constructor.

Typically, when implementing this pattern, only one processor will process the data, and the others will not. This is often used in the UI. For example, in Compose, you might want to pass the onClick handler lambda up the hierarchy.

example

Calculate how many banknotes the ATM will withdraw when someone tries to withdraw money. If someone wants to withdraw $58, you can give him 1 $50, 1 $5, and 3 $1 bills, for a total of $58. This can be achieved through the Chain of Responsibility pattern:

Kotlin Design Pattern: Chain of Responsibility Pattern

For the sake of simplification, ATMs only support $50, $5, and $1 bills.

First, create an interface:

interface ATM {
    fun withdraw(amount: Int)
    fun setNext(atm: ATM)
}           

Now it's the specific ATM. All the code is very similar.

class ATM50Dollars : ATM {
    private var nextATM: ATM? = null
    override fun withdraw(amount: Int) {
        println("提取 ${amount / 50} 张50美元钞票")
        nextATM?.withdraw(amount % 50)
    }
    override fun setNext(atm: ATM) {
        nextATM = atm
    }
}
class ATM5Dollars : ATM {
    private var nextATM: ATM? = null
    override fun withdraw(amount: Int) {
        println("提取 ${amount / 5} 张5美元钞票")
        nextATM?.withdraw(amount % 5)
    }
    override fun setNext(atm: ATM) {
        nextATM = atm
    }
}
class ATM1Dollar : ATM {
    private var nextATM: ATM? = null
    override fun withdraw(amount: Int) {
        println("提取 $amount 张1美元钞票")
        nextATM?.withdraw(0)
    }
    override fun setNext(atm: ATM) {
        nextATM = atm
    }
}           

In this case, if you don't take out all the money, you might want to throw a mistake as well. A given object may not be able to handle a given task. Prevent this from happening.

Now, let's look at an example of how to use it:

fun main() {
    val atm1: ATM = ATM1Dollar()
    val atm50: ATM = ATM50Dollars()
    atm50.setNext(atm1)
    atm50.withdraw(58) // 1 x 50$ + 8 x 1$

    val atm5: ATM = ATM5Dollars()
    atm50.setNext(atm5)
    atm5.setNext(atm1)

    atm50.withdraw(58) // 1 x 50$ + 1 x 5$ + 3 x 1$

    atm5.withdraw(58) // 11 x 5$ + 3 x 1$
}           

This pattern may make your program more complex, but it reduces code coupling and allows you to adapt quickly to changes in requirements.

This pattern is similar to that of decorators. The only real difference is that one of the processing classes can return the result in the chain of responsibility. Instead, all composite classes must process the data in the decorator.

Read on