Намір
Визначає сімейство алгоритмів, які інкапсулюють один одного і являються взаємозамінними. Стратегія дозволяє алгоритмам варіюватися незалежно від клієнта, який їх використовує.
Також відомий як
Політика
Мотивація
Існує багато алгоритмів для розбивання потоку тексту на рядки. Закодовування усіх цих алгоритмів у класи, які їх потребують не являється бажаним через декілька причин:
- клієнти, які потребують розбивання рядків стають більш складними, якщо вони утримуватимуть код для розбивання. Це робить клієнтів більшими і важчими для підтримки, особливо, якщо вони підтримують багато алгоритмів для розбиття тексту на рядки.
- Різні алгоритми будуть потрібними у різний час. Ми не бажаємо підтримувати множину алгоритмів для розбиття, якщо ми не використуємо усіх.
- Важко додавати нові алгоритми і змінювати існуючі, коли розбиття на рядки являється інтегрованою частиною клієнта.
Ми можемо запобігти цим проблемам, визначаючи класи, які інкапсулюють різні алгоритми розбиття. Алгоритм, який інкапсульований в даний спосіб називається стратегією.
Припустимо клас Composition відповідальний за підтримку і оновлення розбивання рядків тексту, який відображається у переглядачі тексту. Стратегії розбиття на рядки не реалізовуються класом Composition. Замість цього, вони реалізовуються окремо у підкласах абстрактного класу Compositor. Потомки Compositor реалізовують різні стратегії.
- SimpleCompositor реалізовує просту стратегію, яка визначає розбиття за один прохід.
- TeXCompositor реалізовує алгоритм TeX для знаходження позицій розбиття. Ця стратегія намагається оптимізувати розбивання глобально, тобто, один параграф цілком.
- ArrayCompositor реалізовує стратегію, яка обирає розбиття так, що кожен рядок має фіксовану кількість елементів. Наприклад, корисно розбивати множину іконок у рядки.
Composition утримує посилання на об’єкти Compositor. Коли Composition переформатовує свій текст, вона передає дану відповідальність до її об’єкта Compositor. Клієнт композиції Composition вказує, який клас Compositor повинен бути використаним за допомогою встановлення необхідного об’єкта Compositor у Composition.
Застосовуваність
Використовуйте шаблон Стратегії коли:
- велика кількість подібних класів відрізняються тільки їхньою поведінкою. Стратегії забезпечують спосіб конфігурування класу однією з багатьох стратегій.
- Ви потребуєте різні варіанти алгоритму. Наприклад, Ви можете визначачити алгоритм, який відображає різні просторово-часові компроміси. Стратегії можуть бути використаними, коли ці варіанти реалізовуються у якості класової ієрархії алгоритмів [H087].
- Алгоритм використвує дані, про які клієнти не повинні знати. Використуйте шаблон Стратегії для запобігання розкриття складних, специфічних для алгоритму структур даних.
- Класи визначають багато поведінок, і вони проявляються у якості багатьох підвиразах оператора умови. Замість використання багатьох виразів з операторами умови, перемістіть відповідні групи підоператорів у їхні відповідні класи стратегій (Strategy).
Структура
Учасники
- Strategy (Compositor)
- Оголошує загальний інтерфейс до усіх підтримуваних алгоритмів. Context використовує даний інтерфейс для виклику алгоритму, визначеного у ConcreteStrategy.
- ConcreteStrategy (SimpleCompositor, TeXCompositor, ArrayCompositor)
- Реалізовує алгоритм, використовуючи інтерфейс наданий класом Strategy.
- Context (Composition)
- Конфігурується за допомогою об’єкта ConcreteStrategy.
- Підтримує інтерфейс до об’єкта Strategy.
- Може визначати інтерфейс, який дозволяє Strategy отримувати доступ до її даних.
Співпрацювання
- Strategy i Context взаємодіють для реалізації обраного алгоритму. Клас Context може передавати усі необхідні алгоритму дані до стратегії коли він викикається. Як альтернатива, контекст може передавати їх самостійно у якості алгоритму до операцій Strategy. Це дозволяє стратегії викликати контекст, як і вимагалось.
- Контекст пересилає запити від його клієнтів до його стратегії. Клієти зазвичай створюють і передають об’єкт ConcreteStrategy до контексту; після чого, клієнти взаємодіють з контекстом ексклюзивно. Часто існує множина класів ConcreteStrategy для обирання клієнтами.
Наслідки
Шаблон проектування стратагія має наступні переваги і недоліки:
- Множини подібних алгоритмів. Ієрархії класів Strategy визначають фамілію алгоритмів або поведінок для повторного використання контекстів. Успадковування може допомогти винести загальну функціональність алгоритмів.
- Альтернатива створення потомків класів. Успадковування пропонує інший шлях підтримки варіантів поведінки алгоритмів. Ви можете створити потомок класу Context безпосередньо для надання йому іншої поведінки. Але це жорстко закодовує поведінку у Context. Воно змішує реалізацію алгоритму з Context, що робить його важчим для розуміння, підтримування і розширення. І ви не можете варіювати алгоритми динамічно. Ви отримаєте множину подібних класів, різниця яких полягає тільки у алгоритмі або поведінці, яку вони реалізовують. Інкапсуляція алгоритму у окремі класи Strategy дозволяє вам варіювати алгоритм незалежно від його контексту, полегшюючи його перемикання, розуміння і розширення.
- Стратегії зменшують кількість виразів з умовними операторами. Шаблон проектування Стратегія надає альтренативу до умовних виразів для обирання бажаної поведінки. Коли різні поведінки зосереджені в одному з класів, важче запобігти використанню операторів умов для обирання слушної поведінки. Інкапсулювання поведінки в окремий класс Strategy запобігає використання умовних виразів.
Наприклад, без стратегій, код для розбиття на рядки виглядав би наступним чином:
void Composition::Repair ()
{
switch (_breakingStrategy)
{
case SimpleStrategy:
ComposeWithSimpleCompositor () ;
break;
case TeXStrategy:
ComposeWithTeXCompositor () ;
break;
// ...
}
// поєднайте результати з існуючими композиціями, якщо необхідно
}
Шаблон проектування Стратегія запобігає використання даних switch-case виразів делегуванням завдання розбиння рядків до об’єкту Strategy:
void Composition::Repair ()
{
_compositor->Compose () ;
// поєднайте результати з існуючими композиціями, якщо необхідно
}
Код, який містить багато умовних виразів, означає необхідність застосування шаблону Стратегія.
- Вибір реалізацій. Стратегії можуть забезпечувати різні реалізації однієї поведінки. Клієт може обрати з-поміж різних стратегій з різними компромісами часу-простору.
- Клієнти повинні не знати про різні стратегії. Даний шаблон проектування має потенційний недолік у тому, що клієт повинен розуміти, як конкретні класи Strategy відрізняються перед тим, як обрати останній. Клієнти можуть бути усвідомленими у реалізаційних питаннях. Отже ви повинні використовувати шаблон Стратегія тільки коли варіація у поведінці відноситься до клієнтів.
- Перенасичення комунікацій між Стратегією і Контекстом. Інтерфейс Strategy розподіляється між усіма класами ConcreteStrategy коли алгоритми, які вони реалізовують являються трівіальними або складними. Отже, ймовірно, що деякі класи ConcreteStrategy не будуть використовувати усю інформацію передану до них через їхній інтерфейс; в найпростішому випадку об’єкт ConcreateStrategy може взагалі її не використовувати! Це означає, що під час створення і ініціалізації контекстом параметрів, які ніколи не будуть використаними. Якщо це являється проблемою, тоді вам необхідно зменшити зв’язок між Strategy i Context.
- Збільшене число об’єктів. Стратегії збільшують кількість об’єктів у програмі. Інколи ви можете зменшити надлишок, реалізовуючи стратегії у якосі об’єктів стану, які контекст може розділити з іншими об’єктами. Будь-який залишковий стан обробляється контекстом, який передає його у кожному запиті до об’єкта стратегії. Розподілені стратегії не повинні обробляти стани між викликами. Шаблон проектування Легка Вага (000) описує цей підхід більш детально.
Реалізація
Розглянемо наступні питання реалізації:
- Визначення інтерфейсів класів Strategy i Context. Інтерфейси Strategy i Context повинні надавати класу ConcreteStrategy ефективний доступ до будь-яких необхідних даних з контексту і навпаки.
Один підхід полягає у тому, щоб Context передавав дані у параметрах до операцій Strategy — іншими словами, надавати дані стратегії. Це тримає Strategy i Context відокремлено. З іншого боку, Context може передавати дані, які Strategy не потребує.
В іншій техніці контекст передає себе у якості аргументу, і стратегія опитує дані з контексту безпосередньо. Альтернативно, стратегія може зберігати посилання на її контекст, запобігаючи потребу передавити будь-які дані взагалі. В будь-якому випадку стратегія може запитувати саме те, що їй необхідно. Але тепер Context повинен визначати більш детальний інтерфейс до його даних, що спаровує стратегію і контекс ближче.
Потреба в певному алгоритмі і його потреби до даних будуть визначати найбільш правильну техніку.
- Стратегії у якості параметрів шаблону. У C++ шаблони можуть бути використаними для конфігурування класу стратегією. Ця техніка застосовувана тільки якщо (1) стратегія може обиратися під час компілювання, і (2) вона не потребує зміни під час виконання програми. У даному випадку, клас для конфігурування (тобто Context) визначається у якості шаблонного класу, який має клас Strategy у якості параметру:
template <class AStrategy>
class Context
{
public:
void Operation() { theStrategy.DoAlgorithm () ; }
// ...
private:
AStrategy theStrategy ;
} ;
Клас конфігурується за допомогою класу Стратегії під час створення примірника:
class MyStrategy
{
public:
void DoAlgorithm() ;
} ;
Context <MyStrategy> aContext ;
Разом з шаблонами, немає потреби визначати абстрактний клас, який буде визначати інтерфейс до Стратегії. Використання стратегії у якості параметра шаблону також дозволяє вам зв’язувати стратегію з її контекстом статично, що може збільшити ефективність.
- Виконуючи об’єкти Стратегії не обов’язковими. Клас контексту може бути спрощений, якщо правильніше не мати об’єкт стратегії. Контест перевіряє чи він має об’єкт стратегії перед тим як використовувати його. Якщо присутній, тоді контекст використовує його як зазвичай. Якщо немає стратегії, тоді Context виконує дії за умовчанням. Перевага даного підходу полягає у тому, що клієнти зовсім не потребують працювати з об’єктами Strategy, окрім як їм необхідно виконати нестандартні дії.
Приклад коду
Ми надамо високорівневий код для прикладу з секції Мотивації, який заснований на реалізації класів Composition i Compositor у InterViews [LCI+92].
Клас Composition підтримуєє колекцію примірників Component, який представляє текст і графічні елементи у документі. Композиція організовує об’єкти компонентів у рядки, використовуючи примірники дочірніх класів Compositor, які інкапсулюють стратегію розбиття на рядки. Кожен компонент має асоційований натуральний розмір, можливість до розтягування і можливість до зменшення. Можливість до розтягування визначає на скільки компонент може вирости за межі його початкового розміру; можливість до зменшення полягає у можливості об’єкта зменшуватися відносно початкового розміру. Композиція передає ці значення у композитор, який використовує їх для визначення найкращої позиції у рядку.
class Composition
{
public:
Composition (Compositor*) ;
void Repair () ;
private:
Compositor* _compositor ;
Component* _components ; // список компонентів
int _componentCount ; // кількість компонентів
int _lineWidth ; // ширина рядка Композитора
int* _lineBreaks ;
// позиції розбиття
// у компонентах
int _lineCount ; // кількість рядків
} ;
Коли необхідний новий макет, композиція опитує його композитора для визначення місць розбиття. Композиція передає композитору три масиви, які визначають початкові розміри, можливості розтягування, і можливості зменшення компонентів. Вона також передає кількість компонентів, яка ширина рядка і масив, який композитор заповняє композицією кожного розбиття. Композитор повертає кількість обчислених розбивань.
Інтерфейс композитора дозволяє композиції передавати композитору усю необхідну інформацію. Це є приклад “передавання даних до стратегії”:
class Compositor
{
public:
virtual int Compose (
Coord natural[], Coord stretch[], Coord shrink[],
int componentCount, int lineWidth, int breaks[]
) = 0;
protected:
Compositor () ;
} ;
Зверніть увагу, що Compositor являється абстрактим класом. Конкретні потомки визначають специфічні стратегії розбиття.
Композиція викликає її композитора у її операції Repair. Метод Repair спочатку ініціалізує масиви з початковим розміром, можливостями розтягування і можливостями зменшення кожного компоненту (деталі яких ми оминаємо для кращої зрозумілості коду). Тоді він викликає композитора для отримання розбиття і на кінець викладає компоненти відповідно до розбиття (яке також пропущене):
void Composition::Repair ()
{
Coord* natural ;
Coord* stretchability ;
Coord* shrinkability ;
int componentCount ;
int* breaks ;
// підготування масивів з бажаними розмірами компонентів
// ...
// визначення розбиття
int breakCount ;
breakCount = _compositor->Compose (
natural, stretchability, shrinkability,
componentCount, _lineWidth, breaks
) ;
// розміщення компонентів відповідно до розбиття
// ...
}
Тепер поглянемо на потомки класу Compositor. Клас SimpleCompositor перевіряє компоненти рядок за раз для визначення місць розбиття:
class SimpleCompositor :
public Compositor
{
public:
SimpleCompositor () ;
virtual int Compose(
Coord natural[], Coord stretch[], Coord shrink[],
int componentCount, int lineWidth, int breaks[]
) ;
// ...
} ;
Клас TeXCompositor використовує більш глобальну стратегію. Він перевіряє параграф цілком, беручи до увагу розмір компонентів і можливість розтягування. Він також намагається надати “колір” параграфу, мінімізуючи пробіли між компонентами.
class TeXCompositor :
public Compositor
{
public:
TeXCompositor () ;
virtual int Compose (
Coord natural[], Coord stretch[], Coord shrink[],
int componentCount, int lineWidth, int breaks[]
) ;
// ...
} ;
Клас ArrayCompositor розбиває компоненти на рядки на рівні інтервали.
class ArrayCompositor :
public Compositor
{
public:
ArrayCompositor (int interval) ;
virtual int Compose (
Coord natural[], Coord stretch[], Coord shrink[],
int componentCount, int lineWidth, int breaks[]
) ;
// ...
} ;
Ці класи не використовують усю інформацію передану у Compose. Клас SimpleCompositor ігнорує можливість розтягування компонентів, беручи тільки їхні початкові розміри до уваги. TeXCompositor використовує усю інформацію передану йому, коли ArrayCompositor ігнорує усе.
Для створення примірника Composition, ви передаєте йому композитор, який бажаєте використати:
Composition* quick = new Composition (new SimpleCompositor) ;
Composition* slick = new Composition (new TeXCompositor) ;
Composition* iconic = new Composition (new ArrayCompositor(100)) ;
Інтерфейс Compositor ретельно розроблений для підтримки усіх макетів алгоритмів, які дочірні класи можуть реалізувати. Ви не бажаєте змінювати даний інтерфейс з кожним новим потомком, через те що це змусить вас змінювати існуючі потомки. В загальному, інтерфейси Strategy i Context визначають як добре шаблон проектування досягає своєї мети.
Відомі використання
ET++ [WGM88] і InterViews використовують стратегії для інкапсуляції різних алгоритмів розбиття, як ми і описали.
У RTL System для оптимізації коду компілятора [JML92], стратегії визначають різні схеми розміщення регістрів (RegisterAllocator) і набір інструкцій політиків планування (RISCscheduler, CISCscheduler). Це надає гнучкість для цільового спрямування оптимізатора на різні машинні архітектури.
Каркас (framework) обчислювального рушія ET++ SwapsManager обчислює ціни для різних фінансових інструментів [EG92]. Його ключова абстрація являється Instrument i YieldCurve. Різні інструменти реалізовуються у якості потомків Instrument. YieldCurve обчислює фактори знижок, які визначають поточне значення майбутніх потоків грошей. Обидва ці класи делегують деяку поведінку до об’єктів Strategy. Каркас надає множину класів ConcreteStrategy для генерації потоків грошей, оцінювання свопів, і обчислення знижок. Ви можете створити нові обчислювальні рушії конфігуруючи Instrument i YieldCurve різними об’єктами ConcreteStrategy. Даний підхід підтримує змішування і співставлення існуючих реалізацій Strategy, так само як визначення нових.
Компоненти Booch [BV90] використовують стратегії у якості аргументів шаблону. Колекція класів Booch підтримує три різні типи стратегії виділення пам’яті: управляємі (виділення пула), контрольований (захищені виділення/звільнення) і не контрольовані (звичайне виділення пам’яті). Дані стратегії передаються у якості параметра шаблона до колекції класів коли створюються їхні примірники. Наприклад, клас UnboundedCollection, який використовує не контрольовану стратегію, створюється у якості UnboundedCollection.
RApp являється системою для інтегрованих мікросхем [GA89, AG90]. RApp повинна розміщати і направляти провідники, які об’єднюють підсистеми мікросхеми. Алгоритми направляння у RApp являються визначиними потомками абстрактного класу Router. Router являється класом стратегії.
Borland ObjectWindows [Bor94] використовує стратегії у діалогових вікнах для запевнення, що користувач вводить правильні дані. Наприклад, числа можуть мати певний проміжок і числове поле вводу повинно приймати тільки числа. Перевірка рядка на правельність може вимагати пошук по таблиці.
ObjectWindows використовує об’єкти Validator для інкапсулювання стратегій валідації. Validator являється прикладом об’єктів Strategy. Поля вводу даних делегують валідаційні стратегі до опціональних об’єктів Validator. Клієнт приєднує валідатор до поля, якщо необхідна перевірка (приклад необов’язкової стратегії). Коли діалог закривається, поле вводу опитує їхні валідатори для перевірки даних. Бібліотека класів постачає валідатори для загальних класів, на подобі aRangeValidator для чисел. Нові клієнт-залежні валідаційні стратегії можуть легко визначатися створюючи потомки класу Validator.
Споріднені шаблони
Легка вага: об’єкти Strategy часто являються хорошими легкими вагами.