Команда (Command): выбор операции во время выполнения

В книге Advanced C++:Programming Styles And Idioms (Addison-Wesley, 1992), Джим Коплен (Jim Coplien) ввел термин функтор, который является объектом, главнейшим назначением которого является инкапсуляция функций (так как "функтор" имеет математическое значение, в этой книге я буду использовать более явный термин функционный объект). Назначение его состоит в отделении выбора вызываемой функции от места, где функция вызывается.

Этот термин упоминается, но не используется в книге Design Patterns. Однако, тема функционных объектов повторяется в нескольких шаблонах этой книги.

Команда (Command)- это функционный объект в чистом виде: метод, который является объектом. [7] При упаковывании метода в объект вы можете передать его в другой метод или объект в качестве параметра, чтобы попросить его выполнить определенную операцию в процессе выполнения вашего запроса. Вы можете сказать, что Command является посыльным (поскольку он предназначен и используется достаточно прямолинейно), который переносит поведение, а не данные.

//: command:CommandPattern.java
package command;

import java.util.*;

import junit.framework.*;

interface Command {
  
void execute();
}

class Hello implements Command {
  
public void execute() {
     
System.out.print("Hello ");
  
}
}

class World implements Command {
  
public void execute() {
     
System.out.print("World! ");
  
}
}

class IAm implements Command {
  
public void execute() {
     
System.out.print("I'm the command pattern!");
  
}
}

// Объект, который хранит команды:
class Macro {
  
private List commands = new ArrayList();
  
  
public void add(Command c) {
     
commands.add(c);
  
}
  
  
public void run() {
     
Iterator it = commands.iterator();
     
while (it.hasNext())
         ((
Command) it.next()).execute();
  
}
}

public class CommandPattern extends TestCase {
  
Macro macro = new Macro();
  
  
public void test() {
     
macro.add(new Hello());
      macro.add
(new World());
      macro.add
(new IAm());
      macro.run
();
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(CommandPattern.class);
  
}
}
// /:~

Основное назначение Command состоит в предоставлении вам желаемого действия в методе или объекте. В приведенном выше примере она предоставляет способ для очереди действий, выполняемых коллективно. В этом случае шаблон позволяет вам динамически создать новое поведение, которое обычно вы можете сделать только при написании нового кода, но в приведенном выше примере может быть сделано при интерпретировании скрипта (смотрите шаблон Интерпретатор, если хотите получить большую сложность).

Другим примером Command является refractor: DirList.java [????]. Класс DirFilter является объектом команды, который содержит действие в методе accept( ), который передается в метод list( ). Метод list( ) определяет, что включать в результат, вызывая метод accept( ).

В книге Design Patterns говорится, что "Команды являются объектно-ориентированной заменой для обратного вызова [8]". Однако, я думаю, что слово "обратный" является существенной частью концепции обратных вызовов. Таким образом, я думаю, обратный вызов возвращается назад к создателю обратного вызова. С другой стороны, объект Command вы обычно создаете и передаете в некоторый метод или объект, и никак иначе не привязываетесь к объекту Command. Такой мой взгляд на этот предмет. Далее в этой книге я собрал группу шаблонов проектирования под заголовком "обратные вызовы".

Стратегия ведет себя сродни классу Command, все наследуется от одного базового класса. Но если вы присмотритесь к Command, вы увидите, что они имеют одинаковую структуру: иерархия функционных объектов. Различие в способе использования этой иерархии. Как видно из refactor:DirList.java, вы используете Command для решения определенной задачи - в данном случае, для выбора файлов из списка. То, что "остается прежним" - это тело метода, который вызывает, а часть, которая меняется, изолирована в функционном объекте. Я рискну сказать, что Command предоставляет гибкость, пока вы пишите программу, в то время, как Strategy предоставляет гибкость во время выполнения. Тем не менее, это выглядит хрупким различием.

Упражнение

  1. Используйте команду из 1-го упражнения 3 главы.

Цепочка откликов (Chain of responsibility)

Пример: служба перевода (локальный->глобальный->Babelfish).

О Цепочке откликов(Chain of responsibility) можно думать как об обобщенном рекурсивном использовании объектов Strategy. Вы можете сделать вызов, а каждая Strategy в связанной последовательности пробует удовлетворить вызов. Процесс заканчивается, когда одна из стратегий завершается удачно или когда заканчивается цепочка. В рекурсии один метод вызывает себя снова и снова, пока не будет достигнута цель. В Chain of responsibility метод вызывает себя, что приводит к вызову (при перемещении вниз по цепочки Strategy) другой реализации метода и т. д., пока процесс не достигнет конца. Процесс окончания определяется либо завершением цепочки (в этом случае возвращается объект по умолчанию; вы способны или не способны предоставить результат по умолчанию, так что вы должны быть способны определить успешность или провал цепочки), либо успешной отработкой одной из Strategy.

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

В GoF есть большое количество рассуждений о том, как должна создаваться цепочка откликов в виде связанного списка. Однако, когда вы взглянете на шаблон, то увидите, что на самом деле это не имеет значения, как содержать цепочку. Это детали реализации. Так как GoF была написана до встраивания Стандартной Библиотеки Шаблонов (STL) в большинство компиляторов C++, то объяснить причины этого объяснения достаточно легко: (1) в то время не было списков и (2) приходилось создавать списки и структуры данных, которые часто изучают в академиях, а идея того, что структура данных должна быть стандартным инструментом, доступным в языке программирования, не возникла у авторов GoF. Я решил, что реализация Chain of responsibility в виде цепочки (реально, как связанный список) ничего не прибавит к решению, а может быть упрощена при использовании стандартного класса Java List, как показано ниже. Более того, вы увидите, что я сделал некоторое усилие для разделения части реализации поддержки цепочки от вариантов Strategy, так что этот код может быть легко использован повторно.

В приведенном выше примере StrategyPattern.java мы пробовали автоматически найти решение. Chain of responsibility предоставляет способ сделать это, объединив объекты Strategy вместе и предоставив им механизм для автоматической рекурсии через каждое звено цепи:

//: chainofresponsibility:FindMinima.java
package chainofresponsibility;

import com.bruceeckel.util.*; // Arrays2.toString()

import junit.framework.*;

// Переносит результирующие данные и
// проверяет удалась ли стратегия:
class LineData {
  
public double[] data;
  
  
public LineData(double[] data) {
     
this.data = data;
  
}
  
  
private boolean succeeded;
  
  
public boolean isSuccessful() {
     
return succeeded;
  
}
  
  
public void setSuccessful(boolean b) {
     
succeeded = b;
  
}
}

interface Strategy {
  
LineData strategy(LineData m);
}

class LeastSquares implements Strategy {
  
public LineData strategy(LineData m) {
     
System.out.println("Trying LeastSquares algorithm");
      LineData ld =
(LineData) m;
     
// [ реальная проверка/вычисления здесь ]
     
LineData r = new LineData(new double[] { 1.1, 2.2 }); // Dummy data
     
r.setSuccessful(false);
     
return r;
  
}
}

class NewtonsMethod implements Strategy {
  
public LineData strategy(LineData m) {
     
System.out.println("Trying NewtonsMethod algorithm");
      LineData ld =
(LineData) m;
     
// [ реальная проверка/вычисления здесь ]
     
LineData r = new LineData(new double[] { 3.3, 4.4 }); // Dummy data
     
r.setSuccessful(false);
     
return r;
  
}
}

class Bisection implements Strategy {
  
public LineData strategy(LineData m) {
     
System.out.println("Trying Bisection algorithm");
      LineData ld =
(LineData) m;
     
// [ реальная проверка/вычисления здесь ]
     
LineData r = new LineData(new double[] { 5.5, 6.6 }); // Dummy data
     
r.setSuccessful(true);
     
return r;
  
}
}

class ConjugateGradient implements Strategy {
  
public LineData strategy(LineData m) {
     
System.out.println("Trying ConjugateGradient algorithm");
      LineData ld =
(LineData) m;
     
// [ реальная проверка/вычисления здесь ]
     
LineData r = new LineData(new double[] { 5.5, 6.6 }); // Dummy data
     
r.setSuccessful(true);
     
return r;
  
}
}

class MinimaFinder {
  
private static Strategy[] solutions = { new LeastSquares(),
        
new NewtonsMethod(), new Bisection(), new ConjugateGradient(), };
  
  
public static LineData solve(LineData line) {
     
LineData r = line;
     
for (int i = 0; i < solutions.length; i++) {
        
r = solutions[i].strategy(r);
        
if (r.isSuccessful())
           
return r;
     
}
     
throw new RuntimeException("unsolved: " + line);
  
}
}

public class FindMinima extends TestCase {
  
LineData line = new LineData(new double[] { 1.0, 2.0, 1.0, 2.0, -1.0, 3.0,
        
4.0, 5.0, 4.0 });
  
  
public void test() {
     
System.out.println(Arrays2.toString(((LineData) MinimaFinder
            .solve
(line)).data));
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(FindMinima.class);
  
}
}
// /:~

Упражнения

  1. Реализуйте Chain of responsibility для создания "экспертной системы", которая решает проблему путем последовательных проб решений одного за другим, пока одно из них не подойдет. Вы должны быть способны динамически добавлять решения в экспертную систему. Тест решения должен проходить простым сравнением строки, но когда решение подходит, экспертная система должна возвращать соответствующий тип объекта ProblemSolver. Какие еще шаблоны понадобятся вам в этом случае?
  2. Реализуйте Chain of responsibility для создания переводчика, который начинает поиск для локальной специализированной системы перевода (которая может знать о специфике области вашей проблемы), затем переходит к более глобальной обобщенной системе и, наконец, переходит в систему BableFish, если предыдущие системы не смогли все перевести. Обратите внимание, что каждое звено цепи может частично перевести то, что ей знакомо.
  3. Реализуйте Chain of responsibility для создания инструмента, помогающего переформатировать исходный код Java, испытывая различные подходы к расстановке переводов строки. Обратите внимание, что с обычным кодом и комментариями необходимо обходится по-разному, возможно придется ввести Дерево Откликов. Также обратите внимание на сходство между этим подходом и шаблоном проектирования Composite. Возможно, более общим описанием этой техники является Composite of Strategies.