Посилання на статті про породжуючі дизайн-патерни та вихідні коди можна знайти тут.
Структурні дизайн-патерни
Композиція (Composite)
Призначення: реалізація деревоподібної структури з можливістю працювати однаково з батьками й нащадками в дереві.
Композиція дозволяє будувати структуровані дані, які можна змінювати в залежності від використання. Мабуть, найкращим прикладом буде XML-структура (приклад наведено з джерела).
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
</catalog>
В такому підході побудови даних основною особливістю є дерево, за допомогою якого ці дані структуруються. Тобто, є якийсь клас catalog
, який при своїй побудові даних використовує дані, які надаються класом book
, а той у свою чергу викликає дані з класів author
, title
тощо, таким чином заповнюючи XML-дані.
За допомогою композиції можна реалізувати свій своєрідний XML-файл з власною структурою.
Нехай потрібно створити базу даних. В цій базі можна зберігати що завгодно, у нашому випадку це будуть смартфони, їхні моделі й деякі характеристики.
class Database
{
public:
Database();
~Database();
virtual string getData() const = 0;
virtual void setData(Database* pdb) = 0;
};
Створивши абстрактний клас Database
, який описує інтерфейс для класів-нащадків, задаємо два поля:
getData()
– повертатиме якісь дані від класів-нащадків, що мають міститися в XML-файлі.setData(Database* pdb)
– в якості параметра приймає вказівник на батьківський клас і його нащадків. Необхідно для того, щоб можна було вставляти дані між мітками:
<book id="bk101"> # відкрити мітку
<author>Gambardella, Matthew</author> # вставити дані (прочитати дані з іншого класу і вставити)
</book> # закрити мітку
Наступний етап – наслідування. Класи-нащадки будуть реалізувати між собою деревоподібну структуру. Створимо клас PhoneDB
, який буде відкривати мітки <Phones>...</Phones>
, між якими будуть інші мітки, записані у вектор vPhones
.
class PhoneDB : public Database
{
public:
PhoneDB();
~PhoneDB();
virtual string getData() const;
virtual void setData(Database* pdb);
vector<Database*> vPhones;
};
За допомогою функції setData(Database* pdb)
заповнюється вектор тими об'єктами, що містять наступні мітки, а в getData()
відкривається перша мітка <Phones>
, після якої циклом заповнюються інші мітки, які міститимуться в блоці <Phones>...</Phones>
. Інші мітки описуються в інших класах-нащадках, а сама послідовність міток задається за допомогою вектора.
string PhoneDB::getData() const
{
string str = "";
str += "<Phones>\
";
if (!vPhones.empty())
{
for (auto it = vPhones.begin(); it != vPhones.end(); ++it)
str += (*it)->getData();
}
else
return str = "Sorry, PhoneDB is empty.";
str += "</Phones>\
";
return str;
}
void PhoneDB::setData(Database* pdb)
{
vPhones.push_back(pdb);
}
Наприклад, наступною міткою може бути <Model>...</Model>
, в якій задається модель смартфона.
class PhoneModel : public Database
{
public:
PhoneModel(int model = 10);
~PhoneModel();
virtual string getData() const;
virtual void setData(Database* pdb);
private:
int m_model;
};
В даному прикладі конструктор задає модель телефона за допомогою змінної m_model
.
string PhoneModel::getData() const
{
string strModel = "";
strModel += "\ <Model>";
switch (m_model)
{
case 0:
strModel += "Nokia";
break;
case 1:
strModel += "Samsung";
break;
case 2:
strModel += "Meizu";
break;
default:
strModel += "Cup with lace";
break;
}
strModel += "</Model>\
";
return strModel;
}
void PhoneModel::setData(Database* pdb)
{
cout << "PhoneModel [set data]: permission denied." << endl;
}
Як ви могли помітити функція setData(Database* pdb)
в даному класі не робить нічого, тільки виводить повідомлення про заборону доступу. Це означає, що блок <Model>...</Model>
не може містити інших міток.
На даному етапі вже можна побудувати XML-структуру, яка матиме наступний вигляд:
<Phones>
<Model>...</Model>
</Phones>
Тобто, якщо у якості елемента вектора vPhones
класу PhoneDB
передати вказівники на об'єкти класу PhoneModel
, отримаємо вищенаведену структуру.
Крім того, можна створити ще один блок, який може містити в собі інші блоки так само як і блок <Phones>...</Phones>
.
class PhoneParameters : public Database
{
public:
PhoneParameters();
~PhoneParameters();
virtual string getData() const;
virtual void setData(Database* param);
vector<Database*> vParameters;
};
У векторі vParameters
задаються об'єкти, які описують наступні блоки та використання аналогічне до першого випадку (PhoneDB
):
string PhoneParameters::getData() const
{
string str = "";
str += "\ <Parameters>\
";
if (!vParameters.empty())
{
for (auto it = vParameters.begin(); it != vParameters.end(); ++it)
str += (*it)->getData();
}
else
return str = "Sorry, parameters are unavalaiabled.";
str += "\ </Parameters>\
";
return str;
}
void PhoneParameters::setData(Database* param)
{
vParameters.push_back(param);
}
Нехай в цьому блоці будуть міститися параметри смартфона: батарея, камера, дата тощо.
class PhoneBattery : public Database
{
public:
PhoneBattery();
~PhoneBattery();
virtual string getData() const;
virtual void setData(Database* pdb);
};
string PhoneBattery::getData() const
{
srand(static_cast<unsigned int>(time(NULL)));
string strBattery = "";
strBattery += "\ \ <Battery>";
int amper = rand() % 4000 + 1500;
strBattery += to_string(amper) + "mA";
strBattery += "</Battery>\
";
return strBattery;
}
void PhoneBattery::setData(Database* pdb)
{
cout << "PhoneBattery [set data]: permission denied." << endl;
}
Все це використовується наступним чином:
int main()
{
Database* db = new PhoneDB(); // інстанціювати об'єкт PnoneDB, тим самим вказуючи що основний блок – <Pnones>
Database* model = new PhoneModel(2); // інстанціювати модель телефона
Database* parameters = new PhoneParameters(); // і об'єкт, що задає набір параметрів
Database* date = new PhoneDate(); // об'єкт, що задає мітку з поточною датою
Database* battery = new PhoneBattery(); // об'єкт що задає мітку з рандомним значенням амперажу батареї
parameters->setData(date); // вставляємо об'єкт, що описує блок дати в вектор, який виводитиметься в блоці параметрів
parameters->setData(battery); // аналогічно для блоку з описом батареї
db->setData(model); // задаємо модель як елемент вектора, що виводитиметься в блоці <Pnones></Phones>
db->setData(parameters); // задаємо параметри як елемент вектора, що виводитиметься в блоці <Pnones></Phones> після блоку моделі
// додавання ще одного блоку моделі й параметрів
Database* model2 = new PhoneModel();
Database* parameters2 = new PhoneParameters();
Database* date2 = new PhoneDate();
Database* battery2 = new PhoneBattery();
parameters2->setData(date2);
parameters2->setData(battery2);
db->setData(model2);
db->setData(parameters2);
cout << db->getData(); // вивід дерева
return 0;
}
Отже, при виклику db->getData()
відбудуться наступні дії:
- відкриття блоку
<Phones>
; - проходження вектора:
- перший елемент вектора –
model
, отже викличеться(*it)->getData()
, тобтоmodel->getData()
, в якій створиться блок<Model>ім'я_моделі</Model>
. Ім'я моделі задавалося конструктором при створенні об'єкту; - другий елемент –
parameters
, який відкриватиме блок<Parameters>
і так само читатиметься вектор, в якому є елементи що задають блок<Battery></Battery>
і<Date></Date>
. Після прочитання вектору мітка параметрів закривається –</Parameters>
; - аналогічно для наступних елементів вектора –
model2
іparameters2
(parameters2
в свою чергу проходить по вектору що міститьbattery2
іdate2
).
- перший елемент вектора –
- закриття блоку –
</Phones>
.
Результат наступний:
<Phones>
<Model>Meizu</Model>
<Parameters>
<Date>Thu Dec 28 10:42:22 2017</Date>
<Battery>2924mA</Battery>
</Parameters>
<Model>Cup with lace</Model>
<Parameters>
<Date>Thu Dec 28 10:42:22 2017</Date>
<Battery>2924mA</Battery>
</Parameters>
</Phones>
Даний приклад не є повністю оптимальний, адже програмісту самому необхідно слідкувати за порядком будування дерева та занесення даних у вектор, однак описує одну з можливостей застосування композиції.
Підсумки
Дизайн-патерн «Композиція» дозволяє реалізувати деревоподібну структуру й однакову роботу для батьківсього класу і всіх класів-нащадків
Алгоритм використання:
- Створити абстрактний клас, який описуватиме інтерфейс похідних класів, таким чином забезпечуючи однакову роботу для всіх класів-нащадків.
- Реалізувати похідні класи, які в свою чергу використовують інші похідні класи, реалізуючи деревоподібну структуру.
Вихідний код до статті доступний за посиланням.
Ще немає коментарів