【Gang of Four】デザインパターン学習 - Chain of Responsibility


Chain of Responsibility - 責任の連鎖

目次
親子関係のあるオブジェクトにおいて、処理を行うオブジェクトを親子間で柔軟に決定することができるようにするのが本パターンか?

目的

1つ以上のオブジェクトに要求を処理する機会を与えることにより、要求を送信するオブジェクトと受信するオブジェクトの結合を避ける。受信する複数のオブジェクトをチェーン状につなぎ、あるオブジェクトがその要求を処理するまで、そのチェーンに沿って要求を渡していく。

構成要素

・Handler 親子クラスの抽象クラス
・ConcreteHandler Handlerの具象クラス
・Client 使用者

実装

画面を構成する部品で例えると下記のような親子関係があると思います。

ウィンドウ
----ウインドウの中に表示するダイアログ
--------ダイアログの中にあるボタン
----ウインドウの中にあるボタン

どれかのオブジェクトを操作して不具合が発生した場合に、対応処理を行うオブジェクトを自由に決められるサンプルコードを実装します。

Handler 親子クラスの抽象クラス

共通クラス
全部のオブジェクトのスーパークラスです。
Viewをインスタンス化した際にmessageTypeへNormal以外を設定したものは、当該オブジェクト内で不具合に対する処理を実行します。
Normalを設定したViewは親Viewへ処理を任せます。そのまた親Viewが...繰り返し

View.kt
package chainofresponsibility

abstract class View(private val parent: View?, private val messageType: MessageType) {
    enum class MessageType {
        Normal,
        Warning,
        Danger
    }

    protected fun handleHelp() {
        if (hasMessage()) {
            helpLogic()
        } else {
            parent?.handleHelp()
        }
    }

    abstract fun helpLogic()

    private fun hasMessage(): Boolean {
        val ret = MessageType.Normal != messageType

        if (ret) {
            createDialog()
        }
        return ret
    }

    private fun createDialog() {
        when (messageType) {
            MessageType.Warning -> {
                print("警告ダイアログ:")
            }
            MessageType.Danger -> {
                print("エラーダイアログ:")
            }
        }
    }

}

ConcreteHandler Handlerの具象クラス

ウインドウ

Window.kt
package chainofresponsibility

class Window(parent: View?, messageType: View.MessageType): View(parent, messageType) {

    override fun helpLogic() {
        println("ウインドウに起因する不具合!")
    }
}

ダイアログ

Dialog.kt
package chainofresponsibility

class Dialog(parent: View?, messageType: View.MessageType): View(parent, messageType) {

    override fun helpLogic() {
        println("ダイアログに起因する不具合!")
    }
}

ボタン
actionメソッドに0以外を渡すと不具合が発生します。

Button.kt
package chainofresponsibility

class Button(parent: View?, messageType: View.MessageType): View(parent, messageType) {

    fun action(arg: Int) {
        if (arg == 0) {
            println("正常終了")
        } else {
            handleHelp()
        }
    }

    override fun helpLogic() {
        println("ボタンに起因する不具合!")
    }
}

Client 使用者

action(1)で不具合が発生します。下記のサンプルコードではbutton1で発生した不具合はwindowがキャッチし、button2で発生した不具合はdialogがキャッチするようになっています。
dialogのメッセージタイプをNormalへ変更すればwindowがキャッチするようになります。

Client.kt
package chainofresponsibility

class Client {
    init {
        // ウインドウ
        val window = Window(null, View.MessageType.Danger)
        // ウインドウ直下に配置されたボタン
        val button1 = Button(window, View.MessageType.Normal)
        // ウインドウ直下に配置されたダイアログ
        val dialog = Dialog(window, View.MessageType.Warning)
        // ダイアログに配置されたボタン
        val button2 = Button(dialog, View.MessageType.Normal)

        button1.action(0)
        button1.action(1)
        button1.action(0)

        button2.action(0)
        button2.action(1)
        button2.action(0)
    }
}

出力結果

[out-put]
正常終了
エラーダイアログ:ウインドウに起因する不具合!
正常終了
正常終了
警告ダイアログ:ダイアログに起因する不具合!
正常終了

このパターンはどうやろう…

パッと見ではそのうちどの親オブジェクトがハンドリングするのか管理しきれなくなって結局バグを生み出しそうですが、そもそもの主旨がメッセージを受信するオブジェクトを知る必要がないようにするパターンなので、それで良いんでしょうか。
うーん。