Chain Of Responsibility (CoR) – это поведенческий шаблон проектирования. В основном я использовал шаблон проектирования «Цепочка ответственности», потому что он помогает нам достичь одной из самых важных целей в мире программирования: «Чем больше атомарных частей, тем более управляемыми являются исходные коды».

Большинство систем состоят из подсистем, которые взаимодействуют друг с другом. У каждой подсистемы есть свои обязанности, и они просто выполняют свои обязанности правильно. Когда эта подсистема собрана, возникает инженерная система, облегчающая нашу жизнь.

Типичная схема цепочки ответственности представлена ​​выше. Как видите, система состоит из трех обязанностей (можно и больше). Но клиентской стороне не нужно бороться, чтобы понять, как работает система в целом. Вместо того, чтобы знать все детали системы, клиентская сторона просто требует обработки из закрытого ящика. Когда закрытый ящик получает запрос, все подсистемы последовательно отрабатываются предыдущими. Я имею в виду, что каждое кольцо в цепочке сначала выполняет свои обязанности, а затем вызывает следующее, которое связано с помощью функции bind(next: Compiler).

Вы когда-нибудь задумывались о структуре данных Linked List? Возможно, вы не только подумали, но и написали пример из него или даже использовали его в реальном проекте. Если у вас есть опыт работы с Linked List, вам может быть легче понять, какую логику мы использовали, когда создавали цепочку ответственности. Подсистемы, охватываемые родителем, который называется цепочкой ответственности, связаны друг с другом через указатель (или, как вы говорите, экземпляр класса). Каждая подсистема знает только другую подсистему, которая находится рядом с ней.

Несколько преимуществ использования цепочки ответственности приведены ниже:

· Он изолирует сложные детали компонента от клиентской стороны.

· Он предоставляет широкие возможности для применения принципа единой ответственности.

· Это одновременно уменьшает Сцепление и повышает уровень Сплоченности.

· Он предоставляет повторно используемые компоненты, которые атомарны и полностью посвящены одной задаче.

Поскольку мы являемся разработчиками программного обеспечения, вероятно, у большинства из нас есть некоторая информация о том, как строится исходный код. Как мы все знаем, исходный код компилируется компилятором, который представляет собой специальное низкоуровневое приложение, созданное для преобразования исходного кода в другую форму.

Чем ближе исходный код к человеческому восприятию, тем дальше он к компьютерному восприятию. Таким образом, каждый исходный код должен быть скомпилирован на машинный язык (или интерпретируемый язык), чтобы работать на физическом устройстве.

Не волнуйся! Я не буду углубляться в компиляторы вроде теории автоматов. На самом деле эти темы не имеют отношения к этой книге, и вам не нужно иметь прочную базу знаний по ним, чтобы создавать надежные программные приложения. Мы собираемся создать только реальные приложения, которые имитируют компилятор и его подсистемы, чтобы понять, что такое цепочка ответственности и как она используется.

Как программист, мы очень часто компилируем исходный код. Может быть, тысячи тысяч тысяч раз в день. Но, как мы видим на картинке выше, компиляция — не простой процесс. Несмотря на то, что компиляция — это действительно тяжелый и сложный процесс, нам не нужно об этом думать. Вместо того, чтобы мучиться со сложными деталями компиляции, мы просто требуем от компилятора. Обычный компилятор имеет подсистемы, показанные выше. Все они имеют разные обязанности и играют разные роли во время компиляции исходного кода. Один из важных моментов, который следует знать, это то, что подсистемы компилятора работают последовательно. По тому же принципу работает и схема «Цепочка ответственности».

Мы долго разговаривали, позвольте мне перейти к делу. Возможно, вы запутались, но как только вы напишете пример, который будет приведен, у вас не будет никаких проблем. Теперь, когда вы читали о компиляторах с тех пор, как только подошли к этому разделу, мы могли бы также создать приложение Kotlin, которое имитирует компилятор с помощью шаблона цепочки ответственности. Через несколько мгновений все станет ярче, чем раньше.

Начнем с класса Compiler, который является родительским классом подсистем.

abstract class Compiler {
    /* The base class has a member which is the same type of own
       in order to handle the next one of the chain. (See also 
       'Linked list') */

    protected var next: Compiler? = null

    fun bind(next: Compiler): Compiler {

        this.next = next;   //See also Fluent Interface pattern.
        return this;
    }

    abstract fun handle()
}

Для родительского класса настолько важно иметь член того же типа, что и его собственный тип, что шаблон цепочки ответственности не может быть установлен без него. Поскольку элемент содержит ссылку на следующую подсистему текущей подсистемы. Итак, от начала до конца все кольца, имеющиеся в цепочке, соединяются последовательно.

class LexicalAnalyzer : Compiler() {
    override fun handle() {
        println("The lexical analysis is being made...")

        next?.handle()
        
    /* It is so important to proceed the next one after current task 
       is finished. Thus, client side won't have to think about   
       which one must be called now! */
    }
}

class IntermediateCodeGenerator : Compiler() {
    override fun handle() {
        println("The intermediate code is being produced...")
        next?.handle()
    }
}

class TargetCodeGenerator : Compiler() {
    override fun handle() {
        println("The target code has been produced...")
        next?.handle()
    }
}

Чтобы сэкономить бумагу и упростить мой пример, я не стал создавать класс для всех подсистем компилятора. Достаточно трех штук! Все части расширяют класс Compiler и реализуют функцию handle(), в которой должны быть описаны обязанности подсистемы. . Обязанности, применяемые производными классами, должны быть атомарными и допускающими повторное использование. (См. также Принцип единой ответственности). В нашем примере обязанности заключаются только в том, чтобы вывести некоторый текст в окно консоли, который показывает нам, что происходит.

fun main(args: Array<String>) {

    val compiler: Compiler = LexicalAnalyzer().bind(
        IntermediateCodeGenerator().bind(
            TargetCodeGenerator()
        )
    ).also { it.handle() }
}

/* OUTPUT:
  The lexical analysis is being made...
  The intermediate code is being produced...
  The target code has been produced...
 */

Все, что вам нужно сделать, это создать экземпляры из классов и связать их друг с другом в правильной последовательности. При вызове функции handle() из переменной будут выполнены все кольца в цепочке. Таким образом, несколько операций, являющихся частью большой, будут выполняться без усложнения. Кроме того, мы можем использовать эти классы отдельно столько, сколько нам нужно. Или мы можем легко изменить последовательность классов в цепочке, чтобы получить другой результат. Этот подход можно использовать многократно, поскольку он соответствует принципу единой ответственности.

Удачи :)