Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Шаблони проектування.docx
Скачиваний:
3
Добавлен:
25.11.2019
Размер:
73.72 Кб
Скачать
  • Шаблони проектування. Загальні відомості. Шаблони створення об’єктів

Шаблон проектування („паттерн”) – це придатна для багаторазового використання заздалегідь заготовлена загальна проектна чи архітектурна ідея, що охоплює сімейство подібних між собою ситуацій, що часто виникають при розробці різноманітних програмних систем. Фонди алгоритмів та програм, бібліотеки підпрограм чи класів, компоненти ActiveX тощо є технологіями повторного використання програмних одиниць, доведених до реалізації: програміст може або вставляти в свій код фрагменти коду, розробленого іншими програмістами, або викликати функції з інших програмних одиниць. Шаблон проектування не є закінченою, готовою для використання зі свого продукту програмною одиницею – це лише ідея того, як влаштувати свою програму. Використання шаблонів не звільняє від необхідності самостійно розробляти проект і тим більше не звільняє від кодування, але дає при цьому орієнтир, підказує, де шукати гарне рішення і тим самим значно полегшує проектування.

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

Останнім часом набуло поширення поняття антишаблону – це типовий, часто наявний в практиці роботи різних програмістських колективів зразок того, як не треба робити.

Шаблони низького рівня, сильно прив’язані до конкретної мови програмування, називаються ідіомами.

Ідіома – це заготовка фрагменту програмного коду, що показує спосіб узгодженого використання конструкцій мови програмування для розв’язання типової задачі.

  • Приклад Ідіома знаходження найбільшого елементу в масиві

int *pa;

int n;

//skipped: input n, allocate memory and input the elements

int nMaxIdx = 0;

for( int i = 1; i < n; ++i ) {

if( pa[i] > pa[nMaxIdx] ) nMaxIdx = i;

}

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

Ідіоми – найпростіший різновид шаблонів.

Надто сильно прив’язані до особливостей конкретної мови, що зменшує їх універсальність, здатність до перенесення в інші контексти.

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

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

Термін „шаблон проектування” прийшов в комп’ютерні науки з містобудування.

В 1970-х роках американський архітектор Кристофер Александер помітив, що безліч різноманітних рішень, що приймаються будівничими інженерами при плануванні міст „в роздріб” (і вимагають щоразу окремих мислительних зусиль) можна розділити на відносно невелику кількість типових випадків.

Наприкінці 1980-х років Кент Бек та Вард Канінгем, які розробляли графічні оболонки на базі мови Smalltalk, послалися на роботи Александера як на джерело своєї ідеї класифікувати типові проектні рішення щодо програмного продукту.

Джеймс Коплін в 1991 р. опублікував книгу про ідіоми програмування мовою С++, де ввів термін „ідіома”, навів каталог ідіом (які на той час вже стихійно склалися в програмістській практиці, але до того часу не становили самостійного предмету вивчення).

Тріумф ідеї шаблонів пов’язаний з виходом в 1991 р. книги ,Еріха Гамми, Ричарда Хелма, Ральфа Джонсона та Джона Вліссідеса (їх творчий колектив відомий в світі як „банда чотирьох”) книги, в назву якої винесено поняття шаблонів проектування.

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

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

Кожен описаний в літературі шаблон має власну назву, що дозволяє розробникам, спілкуючись між собою, не витрачати час на детальний опис своєї ідеї, а передати її зміст, назвавши застосовані шаблони.

Завдяки шаблонам виникає спільне для різних розробників поняттєве поле, що сприяє комунікації між ними.

Знання розроблених шаблонів не звільняє програміста від потреби думати та приймати власні рішення:

по-перше, треба думати хоча б над вибором належного шаблону,

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

  • Найпростіші шаблони

  • Оболонка ресурсу

Інша назва: отримання ресурсу як ініціалізація.

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

Далі спираємося на специфіку мови С++.

Типовою для різних програм є потреба захопити деякий ресурс (контекст графічного пристрою, дескриптор файлу, семафор тощо) при вході в деякий блок коду та гарантувати автоматичне звільнення ресурсу при виході з цього блоку.

Ідея: оголошуєтья спеціальний клас (назвемо його, наприклад, CResource), в ньому член даних (типова назва m_pResource), що є покажчиком на ресурс або дескриптором ресурсу.

В конструкторі класу CResource захоплювати ресурс та присвоювати його дескриптор або адресу в член m_pResource, в деструкторі знищувати (звільняти) ресурс, на який вказує m_pResource.

Клієнтський блок коду тепер не захоплює ресурс самостійно в явному вигляді, а створює об’єкт класу CResource (нехай ім’я цього об’єкту — resource).

Оскільки створення об’єкту завжди починається з виклику конструктору, це означає, що конструктор новоствореного об’єкту захопить ресурс та збереже його в члені m_pResource цього об’єкту.

Мова С++ гарантує, що при виході з блоку клієнтського коду локальні об’єкти знищуються з викликом деструкторів.

При завершенні блоку коду викликається деструктор для об’єкту resource та звільняє ресурс.

  • Тимчасове сховище з відновленням

Підхід до реалізації суттєво спирається на наявність в мові програмування механізму автоматичного знищення об’єктів з викликом деструкторів.

Уявімо, що при вході в деякий блок коду потрібно деякій змінній, що відповідає за важливий параметр функціонування системи (нехай її ім’я — nParam), присвоїти нове значення, а при виході з блоку — відновити попереднє.

Реалізація цієї дії стандартними засобами мови програмування очевидна:

{ // початок блоку коду, де треба змінити значення параметру

int nParamOld = nParam; // копія старого значення

nParam = 37; // встановити нове значення

// основна частина блоку

nParam = nParamOld; // відновити попереднє значення

}

Недолік: програміст може забути написати останній оператор присвоювання, тоді виконання програми продовжиться з «зіпсованим» значенням параметру.

Оголосимо клас, об’єкт якого зберігає копію старого значення параметра та покажчик на змінну, де зберігається параметр.

Конструктор цього класу має два аргументи: посилання на змінну-параметр та нове значення.

Конструктор копіює поточне значення параметру в змінну-член об’єкта, а другій змінній-члену присвоює адресу змінної-параметра, після чого присвоює змінній-параметру нове значення.

При виході з блоку коду буде автоматично викликано деструктор такого об’єкта, який має присвоїти в змінну-параметр (її адреса зберігається в об’єкті) старе значення (яке теж зберігається в об’єкті).

Крім того, для зручності роботи з праметрами різних типів цей клас краще за все зробити шаблонним (англ. template).

  • Приклад

template<typename T>

class CSaveAndRestore {

private:

T m_oldValue;

T *m_pVariable;

public:

CSaveAndRestore(T &variable, T newValue):

m_pVariable(&variable),

m_oldValue(variable)

{

variable = newValue;

}

~CSaveAndRestore() {

*m_pVariable = oldValue;

}

};

int main() {

int nParam = 0;

// блок, де параметру тимчасово присвоюється

// нове значення, а потім відновлюється старе

{

CSaveAndRestore<int> saveParam(nParam, 37);

cout << nParam << endl; // переконатися, що значення нове

}

cout << nParam << endl; // переконатися, що відновилося старе

return 0;

}

  • Шаблони створення

Створення об’єкту – екземпляру класу може здатися елементарною дією, яка в мові програмування виражається одним рядком.

Проте на практиці часто виникають нетривіальні випадки, що вимагають особливих процедур створення.

  • Одинак (Singleton)

  • Мета шаблону

У деяких класів з самого їх цільового призначення слідує, що в програмі може існувати один і тільки один (варіант: не більше одного) об’єкт даного класу.

Наприклад, у програмі, що має свою підсистему управління вікнами, повинен бути один об’єкт „менеджер вікон”, у програмі, що експортує звіти про результати обрахунків в документи MSWord, повинен бути один об’єкт „інтерфейс MSWord” тощо.

Потрібно забезпечити програмну конструкцію, що гарантує створення лише одного об’єкту заданого класу по запиту клієнтського коду та надає єдину точку доступу для цього.

Шаблон існує в двох варіаціях: з ранньою ініціалізацією та з ініціалізацією по запиту.

  • Ідея реалізації (на прикладі мови C++)

Нехай клас, що повинен мати не більше одного екземпляру, має ім’я CSingleton.

Оголосити в ньому статичний метод функцію (типове ім’я – GetInstance), що повертає покажчик на єдиний екземпляр цього класу.

Також оголосити статичну змінну-член m_pInstance – покажчик на єдиний екземпляр.

Початкове значення цієї змінної – порожній покажчик NULL.

Конструктор класу CSingleton – приватний, що робить неможливим пряме створення об’єктів цього класу з клієнтського коду.

Клієнтський код може лише звертатися до методу GetInstance.

Реалізація методу перевіряє, чи є порожнім покажчик m_pInstance і, якщо так, створює новий об’єкт класу CSingleton та присвоює покажчик на нього в змінну m_pInstance і повертає цей покажчик.

Якщо ж викликаний метод виявив, що покажчик непорожній, то це означає, що клієнтський код вже викликав цей метод раніше, тому метод має просто повернути значення покажчика.

class CSingleton {

private:

CSingleton();

static CSingleton *m_pInstance;

public:

static CSingleton * GetInstance();

// оголошення інших методів

};

CSingleton *CSingleton::m_pInstance = NULL;

CSingleton *CSingleton::GetInstance() {

if( m_pInstance == NULL )

m_pInstance = new CSingleton;

return m_pInstance

}

CSingleton::CSingleton() {

// конструктор може викликатися лише

// коли об’єкт не було створено раніше

assert(m_pInstance == NULL);

// решта конструктора

}

  • Пул об’єктів

Ідея шаблону полягає в тому, що система зберігає набір (т.зв. пул) ініціалізованих та готових для використання об’єктів.

Коли системі потрібен новий об’єкт, він не створюється, а береться з пула.

Коли об’єкт більше не потрібен системі, він не знищується, а повертається в пул.

Цей шаблон варто застосовувати, коли одночасно виконуються такі умови:

  • система в процесі своєї роботи створює та кидає велику кількість об’єктів;

  • при цьому одночасно обробляється небагато з них;

  • ініціалізація нового об’єкту є дорогою операцією (вимагає багато часу та пам’яті).

Можливі різноманітні варіанти цього шаблону.

Якщо в пулі немає жодного вільного об’єкту (всі об’єкти вже взято з пулу),

можна розширювати пул (поповнювати його при потребі новими об’єктами),

можна завершувати роботу системи аварійно,

в багатопоточній системі можна призупинити потік, що звернувся до пулу по об’єкт, до тих пір, поки інший потік не звільнить хоча б один об’єкт.

Можливі небезпеки, пов’язані з шаблоном.

По-перше, система, що взяла об’єкт з пулу, може повернути його в зіпсованому, некоректному стані; тому треба слідкувати за коректністю обробки об’єктів.

По-друге, якщо об’єкти в пулі використовуються для обробки та зберігання таємної інформації, пул може стати каналом витоку інформації (об’єкт, в якому залишив сліди даних один потік чи процес, згодом надається іншому).

  • Прототип

Система зберігає набір заздалегідь проініціалізованих, готових для використання об’єктів — прототипів. Коли клієнтському коду потрібен новий об’єкт, він створюється не «з нуля», а копіюється з об’єкту-прототипу. Кожен об’єкт-прототип повинен підтримувати метод клонування, що повертає покажчик або посилання на новостворену копію.

  • Діаграма класів

using System;

namespace Prototype

{

class MainApp

{

static void Main()

{

// Create two instances and clone each

Prototype p1 = new ConcretePrototype1("I");

Prototype c1 = p1.Clone();

Console.WriteLine ("Cloned: {0}", c1.Id);

Prototype p2 = new ConcretePrototype2("II");

Prototype c2 = p2.Clone();

Console.WriteLine ("Cloned: {0}", c2.Id);

// Wait for user

Console.Read();

}

}

// "Prototype"

abstract class Prototype

{

private string id;

// Constructor

public Prototype(string id)

{

this.id = id;

}

// Property

public string Id

{

get{ return id; }

}

public abstract Prototype Clone();

}

// "ConcretePrototype1"

class ConcretePrototype1 : Prototype

{

// Constructor

public ConcretePrototype1(string id) : base(id)

{

}

public override Prototype Clone()

{

// Shallow copy

return (Prototype)this.MemberwiseClone();

}

}

// "ConcretePrototype2"

class ConcretePrototype2 : Prototype

{

// Constructor

public ConcretePrototype2(string id) : base(id)

{

}

public override Prototype Clone()

{

// Shallow copy

return (Prototype)this.MemberwiseClone();

}

}

}

  • Абстрактна фабрика

Нехай в системі є кілька паралельних ієрархій класів. Наприклад, дві:

Студент (базовий клас) та його підкласи:

  • Фізик;

  • Програміст;

  • Біолог;

Обладнання (базовий клас) та його підкласи:

  • Вольтметр;

  • Комп’юер;

  • Мікроскоп.

Як видно, ці ієрархії незалежні в сенсі відношення наслідування, але між їх класами є певна відповідність: якщо кожному студенту потрібно якесь обладнання, то конкретному студенту-програмісту потрібен комп’ютер, студенту-фізику — вольтметр тощо.

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

Це значить, що процес створення конкретних об’єктів з цих паралельних ієрархій повинен бути прихований від даного алгоритму.

Проблема, яку вирішує шаблон «абстрактна фабрика» полягає в тому, щоб надати згаданому алгоритму можливість створювати об’єкти конкретних типів з паралельних ієрархій, не знаючи цих конкретних типів (тобто не засмічуючи загальний та абстрактний алгоритм зайвою конкретикою).

Рішення полягає в тому, щоби зробити ще одну паралельну ієрархію об’єктів.

На вищому її рівні знаходиться клас «фабрика об’єктів» з методами (для нашого прикладу) «створити студента» та «створити обладнання» — тобто по одному методу «створити» для кожної цільової ієрархії.

Ці методи створення в базовому класі «абстрактна фабрика» залишаємо чистими віртуальними.

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

Наприклад, в класі «фабрика фізиків» метод «створити студента» створює об’єкт класу «фізик», а метод «створити обладнання» створює об’єкт класу «вольтметр» тощо.

Тоді в абстрактний алгоритм достатньо передати в якості параметру конкретний екземпляр фабрики — саме йому абстрактний алгоритм буде адресувати свої запити щодо створення об’єктів.

using System;

class MainApp

{

public static void Main()

{

// Abstract factory #1

AbstractFactory factory1 = new ConcreteFactory1();

Client c1 = new Client(factory1);

c1.Run();

// Abstract factory #2

AbstractFactory factory2 = new ConcreteFactory2();

Client c2 = new Client(factory2);

c2.Run();

// Wait for user input

Console.Read();

}

}

// "AbstractFactory"

abstract class AbstractFactory

{

public abstract AbstractProductA CreateProductA();

public abstract AbstractProductB CreateProductB();

}

// "ConcreteFactory1"

class ConcreteFactory1 : AbstractFactory

{

public override AbstractProductA CreateProductA()

{

return new ProductA1();

}

public override AbstractProductB CreateProductB()

{

return new ProductB1();

}

}

// "ConcreteFactory2"

class ConcreteFactory2 : AbstractFactory

{

public override AbstractProductA CreateProductA()

{

return new ProductA2();

}

public override AbstractProductB CreateProductB()

{

return new ProductB2();

}

}

// "AbstractProductA"

abstract class AbstractProductA

{

}

// "AbstractProductB"

abstract class AbstractProductB

{

public abstract void Interact(AbstractProductA a);

}

// "ProductA1"

class ProductA1 : AbstractProductA

{

}

// "ProductB1"

class ProductB1 : AbstractProductB

{

public override void Interact(AbstractProductA a)

{

Console.WriteLine(this.GetType().Name +

" interacts with " + a.GetType().Name);

}

}

// "ProductA2"

class ProductA2 : AbstractProductA

{

}

// "ProductB2"

class ProductB2 : AbstractProductB

{

public override void Interact(AbstractProductA a)

{

Console.WriteLine(this.GetType().Name +

" interacts with " + a.GetType().Name);

}

}

// "Client" - the interaction environment of the products

class Client

{

private AbstractProductA abstractProductA;

private AbstractProductB abstractProductB;

// Constructor

public Client(AbstractFactory factory)

{

abstractProductB = factory.CreateProductB();

abstractProductA = factory.CreateProductA();

}

public void Run()

{

abstractProductB.Interact(abstractProductA);

}

}

Шаблони проектування: структурні шаблони

Загальне поняття про делегування

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

Загальна ідея: об’єкт вдає, ніби реалізує ті чи інші функції, але насправді сам їх не реалізовує, а віддає на виконання іншому об’єкту.

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

Перевага: гнучкість.

Об’єкт-делегат може підмінятися динамічно; програма-клієнт „бачить” лише об’єкт-оболонку та „спілкується” начебто лише з ним; насправді ж в різні моменти часу об’єкт-оболонка перенаправляє запити клієнта до різних делегатів.

Є можливість через один програмний інтерфейс робити різні набори фактичних дій.

Фактично, делегування продовжує і поглиблює ідею поліморфізму:

при поліморфізмі фактична поведінка об’єкту невідома заздалегідь, один з кількох можливих варіантів обирається в момент створення об’єкту і відтоді залишається незмінним протягом його життя;

при делегуванні об’єкт-оболонка навіть протягом життя може скільки завгодно разів перемикатися між різними втіленнями, тобто об’єктами-делегатами.

Посилюється відділення інтерфейсу об’єкта та його реалізації.

Заголовочний файл (у випадку мови С++), де оголошено члени-дані та внутрішні методи класу повинен бути видимим з модуля, де цей клас використовується.

Якщо робота з об’єктом з клієнтського коду відбувається не прямо, а через об’єкт-оболонку, то клієнтський код може взагалі не бачити оголошення класу делегата з його внутрішніми даними та методами, йому достатньо бачити лише оголошення класу оболонки.

Недолік: цей шаблон дещо зменшує швидкість роботи програми, оскільки виклик кожного методу програмою-клієнтом вимагає однієї додаткової операції – пере адресації виклику до об’єкта-делегата.

В розповсюджених сучасних мовах немає засобів прямої підтримки, які б автоматизували реалізацію делегування, тому програмісту залишається власноруч описувати делегування кожного методу; деякі засоби автоматизації делегування на рівні макросів та препроцесора є для мови С++, також відомі рішення на основі сценаріїв для генерації коду.

#include <iostream>

// Інтерфейс: повністю абстрактний клас;

// Методи лише оголошено та залишено без реалізації

class I {

public:

virtual void f() = 0;

virtual void g() = 0;

};

// одна реалізація інтерфейсу

class A : public I {

public:

void f() { std::cout << "A: вызываем метод f()" << std::endl; }

void g() { std::cout << "A: вызываем метод g()" << std::endl; }

};

// друга реалізація інтерфейсу

class B : public I {

public:

void f() { std::cout << "B: вызываем метод f()" << std::endl; }

void g() { std::cout << "B: вызываем метод g()" << std::endl; }

};

// оболонка: об’єкт, що реалізує інтерфейс

// шляхом переадресації всіх викликів об’єкту за покажчиком m_i

class C : public I {

public:

// Конструктор встановлює делегата

C(I *pi) : m_i (pi) { }

void f() { m_i->f(); }

void g() { m_i->g(); }

// метод, що встановлює іншого делегата

void setDelegate(I *pi) { m_i = pi; }

private:

I * m_i;

};

int main() {

A a;

B b;

C c( &a );

c.f();

c.g();

// підмінити делегата

c.setDelegate(&b);

// ті ж самі виклики роблять інші дії

c.f();

c.g();

return 0;

}