Русский
Русский
English
Статистика
Реклама

Паттерны по-моему chain of responsibility и command



В этой статье описываются два паттерна проектирования из широко известной книги Банда Четырёх на примере конкретного кода. Паттерны описаны в множестве мест, поэтому если вы уже разбираетесь в проблеме, мои объяснения будут вам очевидны и не нужны. Мои тексты для тех, кто отчаялся разобраться в проблеме, или вроде бы разобрался, но интуитивно ощущает дискомфорт, будто что-то не то и не так понял. Я попробую объяснить понятно. Студенты утверждают (причём уже после сдачи экзамена, поэтому у меня есть основания такое мнение считать непредвзятым и неангажированным), что получается понятно.


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

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

Chain of responsibility Цепочка ответственности


почему плюсы
Потому что если не я в начале второго курса, то никто у студентов не проверит, что они понимают, что такое указатель. А я не хочу выпускать студентов, которые не знают, что такое указатель. Они могут потом всю жизнь свою программистскую не работать с указателями и памятью, но программист, который не способен врубиться и понять косвенную адресацию и вообще indirection это в моём понимании не программист. По той же самой причине здесь же, в курсе ООП приходится требовать git, иначе некогда.

#include <iostream>#include <stdio.h>using namespace std;class Handler{public:virtual bool handle(int request) = NULL;};

Вы что-нибудь поняли? Класс называется обработчик, мы видим в нём одну единственную виртуальную функцию без реализации с названием обработать, с единственным параметром с названием запрос, поэтому похоже, что функция будет принимать и обрабатывать число, и возвращать логический true или false. Раз эта функция абстрактная, мы можем догадаться, что она создана только для того, чтобы перекрывать её в классах-потомках. Пожалуй, больше из этого кода пока не выжать.

class DivisionChecker : public Handler{private:int value;Handler *next;public:DivisionChecker(int _value, Handler *_next){value = _value;next = _next;}bool handle(int request){if (request % value == 0)return true;elsereturn next->handle(request);}};

Что мы видим здесь? Класс DivisionChecker, потомок класса Handler, видимо, какой-то реальный обработчик. Мы видим у него в полях какое-то числовое value и какой-то указатель next на другой объект Handler. Но раз класс Handler абстрактный, то next будет хранить указатель на объекты-потомки класса Handler, вполне возможно и другие объекты класса DivisionChecker.
Конструктор DivisionChecker просто инициализирует значениях своих полей, тут ничего интересного, а дальше мы видим перекрывается абстрактная функция handle.
Что же DivisionChecker делает, если его просят обработать какое то число, передав его в качестве параметра request? Мы видим, что он проверяет, делится ли request нацело на то число, которое он сам хранит в value, и если делится, то сразу возвращает true. А если не делится, то он вызывает функцию handle у другого обработчика next, ссылку на которого хранит. Возвращает сам то, что этот другой обработчик вернёт. Переводит стрелки, короче. Делегирует работу. Перенаправляет задание. Настоящий менеджер! А кто же крайним останется?

class DefaultHandler : public Handler{bool handle(int request){printf("%d is prime\n", request);return false;}};

Вот он, наш обычный работяга, DefaultHandler, ещё один потомок класса Handler. У него нет никакой своей property, только унаследованный и перекрытый метод handle. Если у него этот метод вызывают, он сообщает, что переданное ему число является простым и возвращает false.

Теперь давайте посмотрим на основную программу. Что мы видим?

void main(){Handler *queue = new DefaultHandler;for (int i = 2; i < 100; i++)if (queue->handle(i) == false)queue = new DivisionChecker(i, queue);system("pause");}

В ней сначала создаётся переменная-указатель queue, потом создаётся объект класса DefaultHandler и адрес его складывается в queue. Короче говоря, queue указывает на объект DefaultHandler, как-то так:



Потом начинает работать цикл, переменная i получает значение 2, и у объекта queue вызывается метод handle с параметром 2. Чей метод вызывается? Правильно, метод класса DefaultHandler. Что он делает? Сообщает, что число 2 простое, и возвращает false. У нас как раз в цикле стоит проверка, и если возвращено false, то создаётся новый объект, но уже класса DivisionChecker. Этому объекту в конструктор передаётся число 2 и адрес того объекта, куда сейчас указывает переменная queue, то есть адрес старого DefaultHandler. Адрес полученного объекта заносится обратно в переменную queue. В результате этих манипуляций получаем следующее:



Начинает работать следующая итерация цикла i получает значение 3, и снова у объекта queue вызывается метод handle с параметром 2. Но только теперь queue указывает на другой объект и вызывается метод класса DivisionChecker. Что он делает? Проверяет, что 3 не делится на 2, хранящееся у него в value, и раз оно не делится, то вызывает handle у того объекта next, который хранит. А чей это будет метод? Метод многострадального DefaultHandler. Что он делает снова? Сообщает, что число 3 простое, и возвращает false. Это false возвращается обратно в DivisionChecker, а из него обратно в основную программу, как раз к проверке условия. Раз возвращено false, то создаётся новый объект, снова класса DivisionChecker, ему в конструктор передаётся число 3 и адрес того объекта, куда сейчас указывает переменная queue, то есть адрес созданного ранее DivisionChecker. Адрес полученного объекта заносится обратно в переменную queue и получаем такую структуру ссылающихся друг на друга объектов:



Посмотрим ещё одну итерацию цикла, i равно 4, у объекта queue вызывается метод handle с параметром 4, queue указывает на DivisionChecker. Тот проверяет, что 4 не делится на 3, хранящееся у него в value, и раз оно не делится, то вызывает handle у того объекта next, который хранит, а это другой DivisionChecker! Он проверяет, что 4 делится на 2, хранящееся в свою очередь у него в value, и раз оно делится, то сразу возвращает true. Это true через пару возвратов прилетает обратно в основную программу, как раз к проверке условия, условие не срабатывает, поэтому никаких новых объектов не создаётся, и цикл продолжает работу своей дорогой.

В итоге, когда цикл доработает до конца, мы увидим на экране список простых чисел, не превышающих 100, а в памяти окажется такая структура из объектов:



В чём смысл этого паттерна? Где здесь цепочка ответственных, и за что они ответственны? Так вот же, это цепочка обработчиков DivisionHandler! Почему цепочка? Потому что каждый из них знает только следующего. Почему ответственности? Потому что он передаёт следующему ответственность за решение задачи, за исполнение запроса.

Где можно встретить пример такого поведения? Да вот, у нас на лекции можно было раньше до ковидных ограничений. Приходит преподаватель в аудиторию, и ему нужен журнал группы, чтобы вписать отсутствующих. Должен преподаватель знать, у кого сегодня этот журнал? Конечно нет. А что он должен знать? Он должен знать одного старосту, и всё. Преподаватель такой: Эй, староста! Давай гони сюда журнал!. А староста такой встаёт и говорит: А меня сегодня нет, я на госуслугах записался и пошёл вакцинироваться! Но журнал должен быть у зам-старосты! Эй, зам-старосты, давай журнал!. А зам-старосты такая встаёт и говорит: А меня тоже сегодня нет, я корзинку с пирожками понесла бабушке. Но журнал должен быть у зам-зам-старосты! Эй, зам-зам-старосты! ....

Должен преподаватель самостоятельно разбираться в хитросплетениях групповой иерархии власти? Нет, не должен. Он просто обращается к единственной точке входа, где должен выполниться его запрос, а кто именно из цепочки объектов его запрос выполнит, его не особо волнует.

Итак, идея паттерна Chain of Responsibility в том, что вызывая какую-то функциональность у некоторого объекта мы ждём, что она просто выполнится, а на самом деле за этим объектом может находиться цепочка из возможных исполнителей, где каждый исполнитель может или выполнить запрос сам, или обратиться к тому единственному следующему исполнителю, которого он знает. Как пишут в умных книжках, разрывается жёсткая связь между отправителем запроса и его исполнителем (не знаю, говорит вам сейчас эта фраза что-нибудь или нет). Важно, что цепочка исполнителей может формироваться на лету и перестраиваться в процессе работы программы, что у нас и происходит.



Диаграмму классов я взял из книжки. Но рекомендую сначала построить диаграмму классов для нашей программы, потом посмотреть на диаграмму классов паттерна из книжки и кое-что заметить и сопоставить классы друг с другом.

Это классический вариант паттерна Chain of Responsibility, но есть ещё один часто используемый приём, который, как мне кажется, тоже можно к этому паттерну отнести. Давайте посмотрим на другую программу.

#include <iostream>#include <stdio.h>using namespace std;class Handler{public:virtual bool handle(int request) = NULL;};class DefaultHandler : public Handler{public:bool handle(int request){printf("%d is prime\n", request);return false;}};

Как будто, пока всё то же самое. Абстрактный базовый класс, объявляющий одну абстрактную функцию, один потомок, который получая число, сразу объявляет его простым и возвращает false.

class DivisionChecker2 : public DefaultHandler{public:bool handle(int request){if ((request % 2 == 0) && (request != 2))return true;elsereturn DefaultHandler::handle(request);}};

А вот и отличие. Новый потомок, но не Handler, а DefaultHandler! Это значит, что он перекрывает не абстрактный метод handle базового класса Handler, а вполне себе рабочий метод handle класса DefaultHandler! Что же он делает? Он проверяет, делится ли переданное ему число на 2, и если делится, но при этом не является самим числом 2, то сразу возвращает true. А вот если это условие не выполняется, то он как и раньше вызывает другой метод, но не у какого-то другого объекта, а у самого себя же, унаследованный метод DefaultHandler::handle. А дальше?

class DivisionChecker3 : public DivisionChecker2{public:bool handle(int request){if ((request % 3 == 0) && (request != 3))return true;elsereturn DivisionChecker2::handle(request);}};

Новый потомок, и снова не от базового класса, а от DivisionChecker2. Проверяет переданное число на делимость на 3, и вызывает унаследованный метод DivisionChecker2::handle. Думаю, вы уже поняли, что дальше будет ещё два потомка и основная программа:

class DivisionChecker5 : public DivisionChecker3{public:bool handle(int request){if ((request % 5 == 0) && (request != 5))return true;elsereturn DivisionChecker3::handle(request);}};class DivisionChecker7 : public DivisionChecker5{public:bool handle(int request){if ((request % 7 == 0) && (request != 7))return true;elsereturn DivisionChecker5::handle(request);}};void main(){Handler *queue = new DivisionChecker7();for (int i = 2; i < 100; i++)queue->handle(i);system("pause");}

Тут, как видим, никакой цепочки из объектов не создаётся, но программа делает то же самое. И всё равно тут можно увидеть цепочку ответственности, но цепочку не из объектов, а из классов, цепочку предок-потомок, где запрос может быть при определённых условиях обработан в классе-потомке или передан предку в иерархии. Такой приём сплошь и рядом используется при создании обработчиков событий. Там, если нужно изменить поведение какого-то обработчика, типа mouseDoubleClickEvent в Qt, мы в потомке перекрываем этот метод, проверяем в нём, должны ли мы поведение при конкретном событии менять, и если да, то выполняем нужное нам действие, а если нет, то вызываем унаследованный обработчик, передавая управление дальше по цепочке ответственных Chain of Responsibility.

Command Команда


Теперь к паттерну Команда. Один из самых клёвых, на мой взгляд, паттернов, с крутой идей и множеством последствий, к которым она приводит. Как обычно, берём программу, и начинаем читать.

#include <conio.h>#include <map>#include <stack>using namespace std;class CPoint{private:int _x,_y;public:CPoint(int x, int y){_x = x; _y = y;}void move(int dx, int dy){_x = _x + dx; _y = _y + dy;}void report(){printf("CPoint is: %d %d\n", _x, _y);}};

Что мы тут видим? Класс для описания точки, с двумя координатами, конструктором, который координаты инициализирует, методом для сдвига точки (сиречь, изменения координат на заданное приращение), и методом для рисования точки на экране. Как? Вы считаете, что это нельзя назвать рисованием точки в заданной точке экрана? Включите абстрактное мышление и воображение! Пока всё понятно, читаем дальше.

class Command{public:virtual void execute(CPoint *selection) = NULL;virtual void unexecute() = NULL;virtual Command *clone() = NULL;virtual ~Command() {};};

А вот здесь уже всё серьёзно. Абстрактные классы нам зачем даны? Чтобы описать то общее, чем будут объединены все их потомки. Значит, у нас будет много частных случаев Команды, много команд, но объединять их все будет что? То, что у всех их можно будет:
  1. вызывать метод execute, передавая туда указатель на какую-то существующую точку и они будут её казнить;
  2. вызывать метод unexecute непонятного назначения;
  3. вызывать метод clone без параметров, и они будут возвращать указатель на какую-то другую команду.

Негусто, но с абстрактными классами всегда всё так э абстрактно. Читаем дальше.

class Command{public:virtual void execute(CPoint *selection) = NULL;virtual void unexecute() = NULL;virtual Command *clone() = NULL;virtual ~Command() {};};class MoveCommand: public Command{private:CPoint *_selection;int _dx; int _dy;public:MoveCommand(int dx, int dy) {printf("MoveCommand::MoveCommand(%d, %d)\n", dx, dy);_dx = dx;_dy = dy;_selection = NULL;}virtual void execute(CPoint *selection){printf("MoveCommand::execute(CPoint *selection)\n");_selection = selection;if (_selection != NULL){_selection -> move(_dx, _dy);}}virtual void unexecute(){printf("MoveCommand::unexecute()\n");if (_selection != NULL){_selection -> move(-_dx, -_dy);}}virtual Command *clone(){printf("MoveCommand::clone()\n");return new MoveCommand(_dx, _dy);}~MoveCommand() {printf("MoveCommand::~MoveCommand()\n");}};

Наконец, что-то конкретное, а не абстрактное! Это конкретный потомок абстрактного класса Command, а именно класс MoveCommand. У него есть свойство для хранения указателя на какую-то точку, и два целочисленных свойства для хранения смещения. В конструктор передаются и инициализируются свойства и зануляется указатель на точку, а вот дальше посмотрим поподробнее ещё раз:

virtual void execute(CPoint *selection){printf("MoveCommand::execute(CPoint *selection)\n");_selection = selection;if (_selection != NULL){_selection -> move(_dx, _dy);}}

В метод execute у команды передаётся точка. В методе она запоминается во внутреннем указателе и у неё вызывается метод move с тем смещением _dx и _dy, которое хранилось в команде. Спрашивается, зачем кому-то вызывать метод execute, чтобы вызвался метод move у точки, если можно было просто вызвать самостоятельно метод move у той же самой точки? Не понятно. Читаем внимательно дальше.

virtual void unexecute(){printf("MoveCommand::unexecute()\n");if (_selection != NULL){_selection -> move(-_dx, -_dy);}}

Что произойдёт, если вызвать у такой команды метод unexecute? Как видим, команда просто вызывает тот же самый метод move у точки (какой точки? да той, которая должна храниться к этому моменту в указателе _selection), но с обратными знаками смещений. Если для одной и той же точки вызвать сначала execute, а потом unexecute, то координаты точки не изменятся. Это важно.

virtual Command *clone(){printf("MoveCommand::clone()\n");return new MoveCommand(_dx, _dy);}

Ну и наконец, метод clone. Мы видим, что если его вызвать у команды MoveCommand, то он создаёт и возвращает новый, но точно такой же экземпляр класса MoveCommand. Что-то мне это напоминает, какой-то другой паттерн, но я уже забыл, какой именно. Может быть кто-то вспомнит и сможет мне напомнить в комментариях.

Ну как бы и всё. Мы надували щёки, ходили вокруг да около одного и того же метода move у точки. Но ведь чтобы создать и сдвинуть точку, нужно всего лишь вызвать у неё метод move? Как-то так:

CPoint *selection = new CPoint(0,0);selection->move(42, 42);

Нет! Нельзя просто так взять и засунуть лампочку в рот сдвинуть точку! У нас есть ещё целая главная функция, давайте читать её:

void main(){map<char,Command*> commands;commands['a'] = new MoveCommand(-10,0);commands['d'] = new MoveCommand(10,0);commands['w'] = new MoveCommand(0,-10);commands['s'] = new MoveCommand(0,10);CPoint *selection = new CPoint(0,0);selection->report();stack<Command*> history;char key;do{key = _getch(); Command *command = commands[key];if (command != NULL){Command *newcommand = command->clone();newcommand->execute(selection);history.push(newcommand);}if (key == 'z' && !history.empty()){Command *lastcommand = history.top();lastcommand -> unexecute();delete lastcommand;history.pop();}selection -> report();} while (key != 27);system("pause");}

Сначала мы создаём ассоциативный массив для указателей на команды, где привязанные к символам 'a', 'd', 'w' и 's' будут лежать четыре экземпляра класса MoveCommand, отличающиеся друг от друга смещениями. Это будут четыре прообраза для четырёх возможных движений точки во все стороны света. Потом мы создаём собственно точку и рисуем её на экране. А потом мы делаем что-то совсем странное, мы создаём стек для команд. Стек, как известно, это такая несправедливая структура данных, что кто в неё пришёл последний, тот первый из неё вышел. А дальше начинается основной цикл обработки сообщений программы, и в нем:

key = _getch(); Command *command = commands[key];if (command != NULL){Command *newcommand = command->clone();newcommand->execute(selection);history.push(newcommand);}

Читаем с клавиатуры символ, нажатый пользователем и ищем по коду символа в ассоциативном массиве команду. Если такая команда нашлась, то мы делаем три вещи:
  1. вызываем её метод clone, который, как мы помним, создаёт и возвращает новый, точно такой же как прототип экземпляр команды, и я снова забыл, как этот паттерн называется;
  2. у полученного нового экземпляра команды, помещённого в переменную newcommand, вызываем метод execute, передавая в него нашу единственную точку;
  3. новый экземпляр команды заталкивается в стек с говорящим названием history.

if (key == 'z' && !history.empty()){Command *lastcommand = history.top();lastcommand -> unexecute();delete lastcommand;history.pop();}

А вот если нажата клавиша 'z', то делается кое-что хитрое. Из стека history, если он не пустой, вытаскивается верхний объект-команда, у неё вызывается метод unexecute и эта команда удаляется.
За пределами моего обзора осталась перерисовка точки после каждой выполненной команды и выход их цикла по нажатой клавише Esc, но в этом вы, наверняка, и так разобрались сами.

В итоге, что позволяет делать эта программа? Она рисует точку на экране и позволяет перемещать её с помощью клавиш. При этом, если в любой момент нажать клавишу 'z', то последнее действие будет отменено (какое бы оно ни было), а если снова нажать клавишу 'z', то будет отменено предыдущее действие, и так далее.

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

CPoint *selection = new CPoint(0,0);selection->move(42, 42);

Можно! Но дело всё в том, что вызов метода это очень негибкая штука, по сравнению, например, с объектов. Метод можно только вызвать и не вызвать, и всё. А объект, как говорится, является first class citizen. Поэтому вместо такого простого кода нужно использовать вот такой, более сложный:

CPoint *selection = new CPoint(0,0);Command *command = new CMoveCommand(42,42);command->execute(selection);

Суть почти всех с редкими исключениями паттернов
Отнять и поделить. Что-нибудь откуда-нибудь вытащить и инкапсулировать как отдельный объект.

Так вот, суть паттерна Команда в том, что нельзя просто так брать и вызывать метод у объекта. Надо чтобы каждый вызов метода представлял собой отдельный специальный объект! Обратите внимание не метод move превращается в объект класса CMoveCommand, а конкретный вызов этого метода превращается в отдельный объект. Собрались вызывать метод move 20 раз значит надо создать 20 объектов класса CMoveCommand.

Что это даёт и почему это важно. Представьте, что вы директор маленького стартапа разработчиков влоггеров (хотел написать разработчиков-программистов, но, как известно, сегодня эта профессия уже выходит из моды и все дети хотят стать влоггерами). Утром вы приходите на работу и лично подходите к каждому своему сотруднику, и говорите: Арсений, ты сегодня внимаешь сюжет про то-то. Ты, Света, сегодня снимаешь сюжет про сё-то. А ты, Денис, сегодня должен два сюжета снять про то, как они будут снимать сюжеты. Они сразу говорят: Будет сделано, Олег Сергеевич и начинают эээ работать (это у влоггеров всё равно так называется?).
На что это похоже в мире объектов? На прямой вызов метода. Есть тот, кто вызывает, и есть тот, кто сразу исполняет. Директивно, сразу, безо всяких посредников.

Потом вы получили MBA и начинаете внедрять новые технологии непрямого управления. Вместо того, чтобы утром подходить к каждому сотруднику, вы нанимаете ассистента и кожаный диван, и удобно на нём развалившись утром пишете бумажки с поручениями. На одной бумажке задача для Арсения, на другой для Дениса, на третьей для Светланы. Вы остаётесь на диване, а ассистент разносит бумажки по исполнителям, которые точно так же начинают работать.

В чём разница? Между вами и исполнителями появляется бумажка-задание, разрывается эта жёсткая связь между тем, кто даёт задание и тем, кто его исполняет. Исполнитель может припоздать на работу он увидит бумажку с заданием чуть позже и начнёт работать. Директор может уехать на Таити в отпуск, наготовив достаточно бумажек с заданиями ассистент вполне справится с раздачей этих бумажек каждое утро, ничего не понимая в теме. Директор может не думать, кому поручить конкретное задание новомодная agile-команда сама разберётся, кто его будет выполнять. Исполнитель может перенаправить бумажку кому-то другому, если он приболел. Директор не сможет сказать, что он поручал совсем другое, а исполнитель не сможет сказать, что ему ничего не поручали вот она, бумажка с конкретным заданием. Ассистент может по пути создать копию этой бумажки и отправить в налоговую сложить в сейф для истории. Наконец, в дальнем углу сейфа может лежать три конверта с тремя бумажками, как в известном анекдоте, помните ведь он на самом деле про паттерн Команда!

Итак, переводим всю эту аналогию обратно на язык классов, объектов и методов. Каждый конкретный объект-команда это замена одного вызова метода. Да, неудобно: лишний уровень indirection. Но зато с объектом, в отличие от метода, можно делать всё, что угодно: отдать куда-то, получить откуда-то, сохранить, загрузить, скопировать, и так далее. Это позволяет:
  • Разорвать жёсткую связь между тем, кто инициирует действие и тем, кто его будет выполнять. Тот, кто инициирует, не обязан знать того, кто будет выполнять и наоборот.
  • Отложить, разнести во времени инициацию действия и его исполнение. Действия заготовлены заранее и отправлены по нужным адресатам. В час Х действие запускается на выполнение, и в этот может уже даже может быть неизвестно, что это за действие.
  • Сохранять, загружать, логгировать, клонировать, универсально работать со всеми действиями, не зная, что это конкретно за действия.
  • Добавлять новые действия, расширяя, а не изменяя существующий код.

Последний пункт очень важен. Что если я захочу добавить ещё одну команду по клавише 'o' перемещаться в начало координат? Я очевидно добавлю нового потомка для Command: команду ToOriginCommand, я создам один объект для этой команды и помещу его в наш ассоциативный массив. Как изменится при этом основной цикл программы? Никак! Вот в этом вся и фишка основная часть программы ничего не знает про конкретные команды, она никак от них не зависит и никак не меняется при добавлении новых команд. Команды, как обработчики событий, могут быть описаны в любом порядке, это ни на что не влияет. Программа только дописывается, расширяется для добавления новой функциональности, но не изменяется. Помните вторую букву в SOLID? Ну так она как раз про это!

На сдачу, как побочный эффект, мы получаем лёгкость реализации undo, отмены действий. Если всё, что происходит, происходит в результате выполнения последовательности атомарных команд, каждая из которых умеет отменять, откатывать своё действие, то сохранив копии этих действий и их порядок выполнения, мы всегда можем проиграть эту цепочку в обратном порядке и откатиться назад.



Вот диаграмма классов для паттерна Команда из книжки. С Command и ConcreteCommand, я думаю, и так всё ясно, но будет поучительно, если вы попробуйте найти, где у нас в программе Invoker, Client и Receiver. Да, у нас отдельных классов таких нет, но мы же должны за деревьями лес видеть? Найдите в нашей программе те части, которые отвечают за Создание команд (Client), за хранение и вызов команд (Invoker) и за получение и исполнение команд (Receiver), убедитесь, что они достаточно друг от друга независимы.

Пожалуй, на сегодня всё. Сегодня у меня для вас три домашних задания. Во-первых, кто первый нашёл страшную специально оставленную косямбу, может броситься писать об этом комментарий предъявить мне сертификат на +1 балл на ближайшей пересдаче по ООП. Во-вторых, нужно найти несоответствие диаграммы классов нашей первой программы для Chain of Responsibility и такой же диаграмме в книжке GoF. В-третьих, надо в программу для паттерна Command вставить функциональность redo. Если вдруг кому понадобится, то программы лежат здесь.
Источник: habr.com
К списку статей
Опубликовано: 29.01.2021 08:04:08
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Ооп

Паттерны проектирования

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru