Дизайн-патерни: Композиція (С++)

7 хв. читання

Посилання на статті про породжуючі дизайн-патерни та вихідні коди можна знайти тут.

Структурні дизайн-патерни

  1. Адаптер.
  2. Міст.
  3. Композиція.
  4. Декоратор.
  5. Фасад.
  6. Легковаговик.
  7. Проксі.

Композиція (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, який описує інтерфейс для класів-нащадків, задаємо два поля:

  1. getData() – повертатиме якісь дані від класів-нащадків, що мають міститися в XML-файлі.
  2. 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>

Даний приклад не є повністю оптимальний, адже програмісту самому необхідно слідкувати за порядком будування дерева та занесення даних у вектор, однак описує одну з можливостей застосування композиції.

Підсумки

Дизайн-патерн «Композиція» дозволяє реалізувати деревоподібну структуру й однакову роботу для батьківсього класу і всіх класів-нащадків

Алгоритм використання:

  1. Створити абстрактний клас, який описуватиме інтерфейс похідних класів, таким чином забезпечуючи однакову роботу для всіх класів-нащадків.
  2. Реалізувати похідні класи, які в свою чергу використовують інші похідні класи, реалізуючи деревоподібну структуру.

Вихідний код до статті доступний за посиланням.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація