Перш ніж розібратися, що таке змішання, корисно описати проблеми, які він намагається вирішити. Скажімо, у вас є купа ідей або концепцій, які ви намагаєтеся змоделювати. Вони можуть бути якимось чином пов’язані, але вони здебільшого ортогональні - це означає, що вони можуть стояти самі по собі незалежно один від одного. Тепер ви можете змоделювати це за допомогою успадкування, і кожна з цих концепцій походить від якогось загального класу інтерфейсу. Потім ви надаєте конкретні методи у похідному класі, який реалізує цей інтерфейс.
Проблема такого підходу полягає в тому, що ця конструкція не пропонує жодного чіткого інтуїтивного способу взяти кожен із цих конкретних класів та поєднати їх разом.
Ідея поєднань полягає в тому, щоб забезпечити купу примітивних класів, де кожен з них моделює базову ортогональну концепцію, і мати можливість поєднати їх, щоб скласти більш складні класи з просто необхідною функціональністю - начебто лего. Самі примітивні класи призначені для використання в якості будівельних блоків. Це можна розширити, оскільки згодом ви можете додавати інші примітивні класи до колекції, не впливаючи на існуючі.
Повертаючись до C ++, методикою для цього є використання шаблонів та успадкування. Основна ідея тут полягає в тому, що ви з’єднуєте ці будівельні блоки разом, надаючи їх через параметр шаблону. Потім ви зв’язуєте їх ланцюгом, наприклад. через typedef
, щоб сформувати новий тип, що містить потрібну функціональність.
На вашому прикладі скажімо, що ми хочемо додати функцію повтору зверху. Ось як це може виглядати:
#include <iostream>
using namespace std;
struct Number
{
typedef int value_type;
int n;
void set(int v) { n = v; }
int get() const { return n; }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Undoable : public BASE
{
typedef T value_type;
T before;
void set(T v) { before = BASE::get(); BASE::set(v); }
void undo() { BASE::set(before); }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Redoable : public BASE
{
typedef T value_type;
T after;
void set(T v) { after = v; BASE::set(v); }
void redo() { BASE::set(after); }
};
typedef Redoable< Undoable<Number> > ReUndoableNumber;
int main()
{
ReUndoableNumber mynum;
mynum.set(42); mynum.set(84);
cout << mynum.get() << '\n';
mynum.undo();
cout << mynum.get() << '\n';
mynum.redo();
cout << mynum.get() << '\n';
}
Ви помітите, що я вніс кілька змін до вашого оригіналу:
- Віртуальні функції тут насправді не потрібні, тому що ми точно знаємо, який тип нашого складеного класу є під час компіляції.
- Я додав за замовчуванням
value_type
для другого параметра шаблону, щоб зробити його використання менш громіздким. Таким чином, вам не доведеться продовжувати друкувати <foobar, int>
щоразу, коли ви склеюєте шматок.
- Замість створення нового класу, який успадковується від частин, використовується простий
typedef
.
Зауважте, що це має бути простим прикладом для ілюстрації ідеї змішування. Тож він не враховує кутових випадків та смішних звичаїв. Наприклад, виконання, undo
не встановивши жодного числа, швидше за все, буде вести себе не так, як можна було б очікувати.
Як анотація, ця стаття також може бути вам корисною.