Паттерн "стратегия" — это выделение в коде семейства разных алгоритмов, используемых к одному и тому же функционалу объекта, и выведение их всех в единое поведенческое пространство, плюс наделение объектов возможностью использовать любой из этих алгоритмов (любое из этих поведений).
Паттерн "Стратегия" описывает разные способы произвести одно и то же действие, позволяя взаимозаменять эти способы в каком-то объекте контекста.
Паттерны — это не какой-то конкретный код, а способ решения ряда типовых задач в построении кода. Поэтому вариантов в реализации любого отдельного паттерна может быть много и они могут совершенно отличаться друг от друга. Так, например, в алгоритмах STL активно используются коллбэки, а коллбэки можно считать одним из способов реализации стратегии. Например, какой-то объект сортируется разными алгоритмами сортировки, конкретный выбор зависит от каких-то внешних факторов: на маленьких объектах может оказаться эффективной пузырьковая сортировка, а на больших быстрая. Две функции сортировки размещают рядом друг с другом в каком-то месте кода для своего удобства (конечно, им необязательно находиться рядом, просто так будет легче гулять по коду и не заблудиться, факт в том, что два поведения выведено в единое поведенческое пространство и сведено в свою автономию) и потом в зависимости от ситуации нужную подставляют в алгоритм.
Коллбэки — это не только стратегия, они могут представлять из себя и другие паттерны, на них могут быть построены другие паттерны, они одновременно могут оказываться несколькими паттернами сразу, поэтому не поймите неправильно то, что я написал. Смысл в том, что один из вариантов реализации стратегии — коллбэки.
Реализация паттерна может зависеть от парадигмы. Классические варианты примеров пришли к нам из ООП, именно с ними вы в первую очередь встречаетесь в своей литературе и на своих лекциях. И на этой странице будет описан классический вариант реализации паттерна "стратегия", чтобы вы лучше поняли смысл, изучаемого вами. На данный момент вы должны понимать, что "стратегия" — это объединение поведений в свой собственный независимый организм с заданием возможности выбирать любое необходимое поведение из набора поведений этого организма пользующемуся ими объекту.
Теперь мы напишем небольшую программу в классическом варианте реализации. Представьте, что вы работаете в команде программистов, у вашей конторы появился заказ: нужно написать программу, в которой будут крякающие утки-игрушки. Вы приступаете к написанию кода. Пишете Вы, опираясь на ООП, т. е. используете прелести полиморфизма. Сначала создаёте абстрактный класс, от которого производите дочерние…
Всё вроде в порядке: игрушечные утки есть, крякать умеют. Тут заказчик вам говорит: "Хорошо бы, чтобы резиновые утки крякали не «quack», а пищали «piquack»". И Вы такой: "не проблема" и просто переопределяете кряканье резиновой утке:
Постепенно в программу добавляются разные утки: металлические, стеклянные, пластмассовые и т. д. И каждый раз вам приходится что-то где-то переопределять. Например, заказчик просил, чтобы стеклянные утки не крякали. Вы переопределяете метод в соответствующем классе:
В итоге у вас в программе появилось очень много переопределений. Это у нас здесь используется один метод, только крякать, а в вашей программе только методов могло бы быть с десяток или сотню и каждый мог бы быть много раз переопределён под конкретную утку. Это означает, что вы зависите от класса уток, любое исправление способа кряканья означает, что класс утки будет переписываться каждый раз, а это в свою очередь чревато тем, что отлично работающий класс, попав под воздействие обновления, может оказаться классом с ошибками, а последствия этих ошибок могут быть замечены поздно. Один из участников вашей команды проявил невнимательность и поправил действие кряканья на кошачье мяуканье. Просто промахнулся классом. (У вас в программе много типов разных игрушек, есть и котики, я просто не раздуваю программу, оставляя необходимое). Итак, кто-то промахнулся и в итоге вы получили программу, в которой все утки, в которых не переопределён метод крякания, мяукают, а в которых переопределён — крякают. Знаете же, что такое закон подлости? Подходит срок, вы завершили проект, показываете презентацию и… утки начали мяукать. Здорово.
Конечно, это надуманный пример, на практике вряд ли кто-то допускает столь громадные промахи, но закон подлости существует, а зависимость от конкретных классов ничего хорошего не сулит. Локальное изменение кода может приводить к нелокальным побочным эффектам. Например, утки — это плавающие птицы. И пусть у нас были игрушки, мы вполне могли добавить в основной класс умение плавать. Но в то же время мы могли не учесть, что для резиновых и деревянных игрушек плавать нормально, а для стеклянных нет (стеклянные утки добавились в программу не сразу, заранее о них было неизвестно), отчего пришлось бы править метод стеклянным уткам, опять же путём переопределения.
Большое количество переопределений порождает проблему сопровождения кода и поддержки программы.
Наследование в лоб не всегда хорошо подходит для решения задач с задействованием наследования.
Наследование нам не подошло, потому что утиное поведение изменяется в дочерних классах (кто-то просто крякает, кто-то крякает пища), а некоторым дочерним классам даже само поведение не нужно (кто-то молчит, кто-то не плавает). С нашим подходом в лоб, в случае изменения поведения, нам придётся искать все места, где поведение определено и переписывать каждое из них.
Хорошо бы было иметь возможность организовать код так, чтобы крякали только нужные утки, крякали с писком только те утки, которые должны крякать с писком, и молчали те утки, которые должны молчать, чтобы не нужно было трогать классы с утками. Но как этого добиться?
Выделите аспекты приложения, которые могут изменяться, и отделите их от тех, что всегда остаются постоянными.
Выведение таких частей кода в отдельную автономию позволит вам вносить изменения в них и не влиять на работу остального кода.
В нашем случае изменяющимися аспектами являются способы поведения уток (способы крякать, способы плавать). Поэтому мы создадим для них отдельные иерархии. Я буду создавать только одну: для кряканья, чтобы не громоздить код. Создадим абстрактный класс Behavior и будем производить от него всевозможные виды поведений. Это будет класс, никак не зависящий от класса уток.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//Листинг #4
classQuackBehavior{
public:
virtualvoidquack()=0;//виды кряканья уток будут воспроизведены в дочерних классах
classPiskQuack:publicQuackBehavior{//кряканье с писком
public:
voidquack(){
cout<<"piskquack\n";
}
};
classNoQuack:publicQuackBehavior{
public:
voidquack(){
//класс для некрякающих уток
}
};
Таким образом мы избавимся от жёсткой привязки к классам конкретных уток и любое изменение поведения будет означать вмешательство в класс поведения, но не в другие. Разместите этот класс поверх класса игрушечных уток, если делаете всё в одном файле. Вопрос теперь в том, что с этим классом делать и как его использовать. Здесь всё довольно просто: в нашем случае класс поведений должен использоваться классом уток, а чтобы связать эти классы друг с другом, нам необходим связующий экземпляр. Мы должны создать объект от класса поведений и посредством этого объекта выбирать нужное поведение. Фактически наш объект будет своебразным выборщиком. Такой объект нужно обозначить в классе уток. Чтобы можно было объявить объект по типу класса поведений, я и попросил чуть ранее разместить класс поведений выше класса уток. Из класса уток мы должны удалить способы кряканий, потому что эти способы теперь завязаны на единственный класс поведений, а не на разные классы уток.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//Листинг #5
#include <iostream>
usingnamespacestd;
classQuackBehavior{
public:
virtualvoidquack()=0;//виды кряканья уток будут воспроизведены в дочерних классах
//Вызовы временно убраны, потому что методы крякания из уток перемещены в класс поведений
//пока что у уток вообще методов нет
//а мы должны работать с утками
delete tg;
delete tr;
delete td;
}
Поскольку мы работаем с утками, нам нужно крякать именно утками, поэтому мы обращаемся к классу уток, а класс уток дальше самостоятельно использует свой объект, выбирающий поведение. Получается что-то вроде того, что класс уток даёт указание объекту, об этом действии ещё говорят "класс уток делегирует обязанности выбирать поведение своему объекту". Нам сейчас нужно написать сам вызов крякания. Сделать это несложно:
behavior->???// но возникает вопрос, что здесь делать?
}
};
Чтобы вызвать необходимое поведение, мы должны сообщить утке, какое поведение мы желаем от неё увидеть. Сделать это можно с помощью параметра. Использование параметра будет означать, что программист, который пишет программу, слагает с себя обязанности самостоятельной выборки поведений для уток и передаёт эти обязанности вызывающей стороне: пользователю, заказчику, кому угодно. Это ведь удобнее, чем каждый раз переписывать код, чтобы подстраивать под нужное поведение. Так заказал поведение параметром в любое нужное время и радуйся.
behavior=b;//значение параметра обязательно передать объекту-выборщику
b->quack();//выборщик поведения вытаскивает поведение из класса поведений по обозначению параметром *b
}
};
Особое внимание обращаем на то, что мы используем указатели и на то, что параметр обязательно должен быть связываем с объектом, иначе наш выборщик поведения не будет знать, какое поведение пришло параметром. Указатели мы используем, потому что нужно, чтобы работал полиморфизм, нам нужны или указатели или ссылки на объект, с обычными переменными. В данном случае параметр-указатель, поэтому указатели.
Что у нас происходит в листинге #7? Вызывающая поведение утки сторона заказывает аргументом тип поведения утки, этот тип поведения подбирает выборщик поведений, а сам выборщик поведений, являясь объектом основного класса своей иерархии, ищет в своей иерархии подкласс, выражающий то поведение, которое пришло в параметре. Полиморфизм как он есть. Надеюсь, что происходит, вы сейчас уже начали понимать, если не понимали, или понимаете хорошо.
Как с этим работать вызывающей стороне? Разумеется, нужно использовать объект любой игрушки, вызвав у него метод кряканья, подставив в вызов нужное поведение.
C++
1
2
3
4
5
6
7
8
9
10
11
12
//Листинг #8 для main()
QuackBehavior*quack;
SimpleQuack simple_quack;//набор поведений
PiskQuack pisk_quack;
quack=&simple_quack;//выбираем одно из
td->quack(quack);//теперь любая утка крякнет как закажем, сейчас SimpleQuack (простое кряканье)
quack=&pisk_quack;//сейчас с писком
td->quack();
tr->quack();
Полный код, получившейся программы, с небольшим дополнением в виде cout и небольшими изменениями в именовании объектов внутри main для лучшей читаемости вывода на экран и чтения самого кода:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// #Листинг #9
#include <iostream>
usingnamespacestd;
classQuackBehavior{
public:
virtualvoidquack()=0;//виды кряканья уток будут воспроизведены в дочерних классах
behavior=b;//значение параметра обязательно передать объекту-выборщику
b->quack();
}
};
classTreeDuck:publicToyDuck{};
classRubberDuck:publicToyDuck{};
classGlassDuck:publicToyDuck{};
intmain()
{
ToyDuck*td=newTreeDuck();//названия объектов образовано по большим буквам правой части, если что
ToyDuck*rd=newRubberDuck();//что-то вроде аббревиатуры строчными
ToyDuck*gd=newGlassDuck();//gd ==> Glass Duck
QuackBehavior*quack_view;//способ крякать
SimpleQuack simple_quack;//набор поведений
PiskQuack pisk_quack;
cout<<"TreeDuck do simple_quack: ";
quack_view=&simple_quack;//сначала выбираем способ крякать
td->quack(quack_view);//потом не забываем его заказывать в круглых скобках
cout<<"TreeDuck do pisk_quack_quack: ";
quack_view=&pisk_quack;
td->quack(quack_view);//теперь деревянная утка крякает с писком
cout<<"RubberDuck do pisk_quack_quack: ";
rd->quack(quack_view);//теперь резиновая утка крякает с писком
delete td;
delete rd;
delete gd;
}
Как вы видите, подход к написанию программы сильно изменился, но не поведение её. Фактически мы провели полноценный рефакторинг. Человек мог такое проделать даже ничего не зная о паттернах, но такой способ рефакторинга, когда выделяются поведения, алгоритмы, действия или что-то подобное, отделяются от остальной части кода и используются — это и есть то, что называют паттерном "стратегия". Паттерн стратегия" — это не только способ рефакторинга, это и просто написание кода сразу с использованным нами подходом. Сейчас мы просто по ходу дела применили паттерн к нашей первоначальной архитектуре: выделили разные поведения, свели их в одну иерархию и получили возможность задействовать любым причастным объектом. Как я упоминал в начале темы, реализации паттернов могут быть разными, а любой паттерн проектирования — это просто способ решения типовой задачи архитектуры кода.
Если сравнить то, что у нас было: мы были жёстко привязаны к конкретным классам уток, постоянно должны были писать переопределения, причём из-за нескольких поведений на одну утку, могли бы наплодить кучу дополнительных классов, и могли вносить изменения в поведение только на уровне исходников, с тем, что у нас стало: мы перестали быть привязаны к классам уток, переопределения нам теперь совершенно не нужно, любое поведение можно задать любой утке, причём не только на уровне исходного кода, но и во время выполнения программы, избегая множество if…else, можем заказывать поведения, подставляя их в аргументы, легко сделать выводы, хорошо или плохо мы сделали, изменив подход к кодированию. По-моему — очень хорошо заметно, что отрефакторенный код удобнее. Его проще использовать, за ним проще следить (хотя бы потому что не плодится уйма новых классов), а это значит, что и ошибочному влиянию он подвержен намного меньше, чем первый вариант. Кроме того, когда мы вытащили поведение из классов уток и создали им независимость, мы заимели возможность использовать похожие поведения для других типов. Крякают не только утки, коростель-дергач тоже умеет, и если бы вы использовали класс птиц с разделением на виды, то поведение кряканья дергачу, возможно, пришлось бы дублировать, а мы сейчас можем использовать готовое, а не писать снова. Любое новое поведение добавить теперь очень просто: достаточно добавить один класс в иерархию Behavior.
Подводим итоги:
Паттерны — это способы выстроить код программы таким образом, чтобы сделать его гибче и проще в использовании.
Любой отдельный паттерн — это не код, а только вариант решения проблемы с архитектурой программы. Любой один паттерн может иметь много совершенно разных реализаций.
Паттерн стратегия — это решение проблемы, возникающей с появлением множества вариантов поведений, завязываемых на одно действие.
В том случае, если поведения выделяются, выбираются и отделяются от каких-то частей кода и образуют свой собственный поведенческий организм, который потом задействуется для выбора поведений из своего набора, мы имеем дело с паттерном стратегия
Отделение изменяемых аспектов кода от неизменяемых может помочь улучшить архитектуру кода.
Включение объекта одного класса в поле другого класса называется композицией. В нашем случае мы включили объект, воплощающий поведения, в класс, обозначающий уток, т. е. использовали композицию. Это сделало нашу программу гибче: мы легко смогли выбирать любой утке любое поведение, что сначала было сделать сложнее (можете попробовать проделать это на первом варианте программы).
Использовать композицию вместо наследования — хорошо.
Иногда стратегиями называют каждый отдельный класс поведения из иерархии основного класса поведений. Основной класс поведений (В нашем случае класс Behavior) в разных источниках могут называть как суперклассом, так и интерфейсом или контекстом.
Добавить комментарий