Объектноориентированное программирование
Скачать 1.73 Mb.
|
Прототип (Prototype) Прототип (Prototype) шаблон проектирования, порождающий объекты. Назначение: Задаёт виды создаваемых объектов с помощью экземпляра-прототипа и создаёт новые объекты путём копирования этого прототипа. Паттерн Prototype (прототип) можно использовать в следующих случаях: система должна оставаться независимой как от процесса создания но- вых объектов, так и от типов порождаемых объектов. Непосредственное ис- пользование выражения new в коде приложения считается нежелательным (подробнее об этом в разделе Порождающие паттерны); необходимо создавать объекты, точные классы которых становятся известными уже на стадии выполнения программы. Плюсы: 115 для создания новых объектов клиенту необязательно знать их кон- кретные классы; возможность гибкого управления процессом создания новых объек- тов за счет возможности динамических добавления и удаления прототипов в реестр. Минусы: Каждый тип создаваемого продукта должен реализовывать операцию клонирования clone() . В случае, если требуется глубокое копирование объ- екта (объект содержит ссылки или указатели на другие объекты), это может быть непростой задачей. Применение: Для создания новых объектов паттерн Prototype использует прототипы. Прототип – это уже существующий в системе объект, который поддержи- вает операцию клонирования, то есть умеет создавать копию самого себя. Таким образом, для создания объекта некоторого класса достаточно выпол- нить операцию clone() соответствующего прототипа. Паттерн Prototype реализует подобное поведение следующим образом: все классы, объекты которых нужно создавать, должны быть подклассами одного общего абстрактного базового класса. Этот базовый класс должен объявлять интерфейс метода clone() . Также здесь могут объявляться вир- туальными и другие общие методы, например, initialize() в случае, если после клонирования нужна инициализация вновь созданного объекта. Все производные классы должны реализовывать метод clone() . В языке С++ для создания копий объектов используется конструктор копирования, од- нако, в общем случае, создание объектов при помощи операции копирова- ния не является обязательным. Используйте этот шаблон проектирования, когда система не должна за- висеть от того, как в ней создаются, компонуются и представляются про- дукты: инстанцируемые классы определяются во время выполнения, напри- мер с помощью динамической загрузки; для того чтобы избежать построения иерархий классов или фабрик, параллельных иерархии классов продуктов; экземпляры класса могут находиться в одном из нескольких различ- ных состояний. Может оказаться удобнее установить соответствующее число прототипов и клонировать их, а не инстанцировать каждый раз класс вручную в подходящем состоянии. 116 #include #include #include 117 } private: Infantryman( Dummy ) { Warrior::addPrototype( Infantryman_ID, this); } Infantryman() {} static Infantryman prototype; }; class Archer: public Warrior { public: Warrior* clone() { return new Archer( *this); } void info() { cout << "Archer" << endl; } private: Archer(Dummy) { addPrototype( Archer_ID, this); } Archer() {} static Archer prototype; }; class Horseman: public Warrior { public: Warrior* clone() { return new Horseman( *this); } void info() { cout << "Horseman" << endl; } private: Horseman(Dummy) { addPrototype( Horseman_ID, this); } Horseman() {} static Horseman prototype; }; Infantryman Infantryman::prototype = Infantryman( Dummy()); Archer Archer::prototype = Archer( Dummy()); Horseman Horseman::prototype = Horseman( Dummy()); int main() { vector // ... } 118 12.1.5 Одиночка (Singleton) Одиночка (Singleton) паттерн, контролирующий создание единствен- ного экземпляра некоторого класса и предоставляющий доступ к нему Назначение: Часто в системе могут существовать сущности только в единственном экземпляре, например, система ведения системного журнала сообщений или драйвер дисплея. В таких случаях необходимо уметь создавать единствен- ный экземпляр некоторого типа, предоставлять к нему доступ извне и запре- щать создание нескольких экземпляров того же типа. Плюсы: Класс сам контролирует процесс создания единственного экземпляра. Паттерн легко адаптировать для создания нужного числа экземпля- ров. Возможность создания объектов классов, производных от Singleton. Минусы: В случае использования нескольких взаимозависимых одиночек их реализация может резко усложниться. Применение: Архитектура паттерна Singleton основана на идее использования гло- бальной переменной, имеющей следующие важные свойства: 1. Такая переменная доступна всегда. Время жизни глобальной перемен- ной – от запуска программы до ее завершения. 2. Предоставляет глобальный доступ, то есть, такая переменная может быть доступна из любой части программы. Однако, использовать глобальную переменную некоторого типа непо- средственно невозможно, так как существует проблема обеспечения един- ственности экземпляра, а именно, возможно создание нескольких перемен- ных того же самого типа (например, стековых). Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому объекту осу- ществляется через статическую функцию-член класса, которая возвращает указатель или ссылку на него. Этот объект будет создан только при первом обращении к методу, а все последующие вызовы просто возвращают его ад- рес. Для обеспечения уникальности объекта, конструкторы и оператор при- сваивания объявляются закрытыми. 119 // Singleton.h class Singleton { private: static Singleton * p_instance; // Конструкторы и оператор присваивания недоступны клиентам Singleton() {} Singleton( const Singleton& ); Singleton& operator=( Singleton& ); public: static Singleton * getInstance() { if(!p_instance) p_instance = new Singleton(); return p_instance; } }; // Singleton.cpp #include "Singleton.h" Singleton* Singleton::p_instance = 0; Клиенты запрашивают единственный объект класса через статическую функцию-член getInstance() , которая при первом запросе динамически выделяет память под этот объект и затем возвращает указатель на этот уча- сток памяти. В последствии клиенты должны сами позаботиться об осво- бождении памяти при помощи оператора delete Последняя особенность является серьезным недостатком классической реализации шаблона Singleton. Так как класс сам контролирует создание единственного объекта, было бы логичным возложить на него ответствен- ность и за разрушение объекта. Этот недостаток отсутствует в реализации Singleton, впервые предложенной Скоттом Мэйерсом. 12.2 Структурные паттерны проектирования Структурные паттерны рассматривают вопросы о компоновке системы на основе классов и объектов. При этом могут использоваться следующие механизмы: наследование, когда базовый класс определяет интерфейс, а под- классы – реализацию. Структуры на основе наследования получаются ста- тичными; композиция, когда структуры строятся путем объединения объектов некоторых классов. Композиция позволяет получать структуры, которые можно изменять во время выполнения. К структурным паттернам проектирования относятся следующие: адаптер (adapter); мост (bridge); 120 компоновщик (composite); декоратор (decorator); прокси (proxy). 12.2.1 Адаптер (Adapter) Адаптер (Adapter) структурный шаблон проектирования, предназна- ченный для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс. Назначение: Часто в новом программном проекте не удается повторно использовать уже существующий код. Например, имеющиеся классы могут обладать нуж- ной функциональностью, но иметь при этом несовместимые интерфейсы. В таких случаях следует использовать паттерн Adapter. Паттерн Adapter, представляющий собой программную обертку над су- ществующими классами, преобразует их интерфейсы к виду, пригодному для последующего использования. Рассмотрим простой пример, когда следует применять паттерн Adapter. Пусть мы разрабатываем систему климат-контроля, предназначенной для автоматического поддержания температуры окружающего пространства в заданных пределах. Важным компонентом такой системы является темпе- ратурный датчик, с помощью которого измеряют температуру окружающей среды для последующего анализа. Для этого датчика уже имеется готовое программное обеспечение от сторонних разработчиков, представляющее собой некоторый класс с соответствующим интерфейсом. Однако использо- вать этот класс непосредственно не удастся, так как показания датчика сни- маются в градусах Фаренгейта. Нужен адаптер, преобразующий темпера- туру в шкалу Цельсия. Плюсы: Паттерн Adapter позволяет повторно использовать уже имеющийся код, адаптируя его несовместимый интерфейс к виду, пригодному для использо- вания. Минусы: Задача преобразования интерфейсов может оказаться непростой в слу- чае, если клиентские вызовы и (или) передаваемые параметры не имеют функционального соответствия в адаптируемом объекте. Применение: 121 Пусть класс, интерфейс которого нужно адаптировать к нужному виду, имеет имя Adaptee . Для решения задачи преобразования его интерфейса паттерн Adapter вводит следующую иерархию классов: виртуальный базовый класс Target . Здесь объявляется пользователь- ский интерфейс подходящего вида. Только этот интерфейс доступен для пользователя. производный класс Adapter , реализующий интерфейс Target . В этом классе также имеется указатель или ссылка на экземпляр Adaptee . Паттерн Adapter использует этот указатель для перенаправления клиентских вызовов в Adaptee . Так как интерфейсы Adaptee и Target несовместимы между со- бой, то эти вызовы обычно требуют преобразования. Приведем реализацию паттерна Adapter. Для примера выше адаптируем показания температурного датчика системы климат-контроля, переведя их из градусов Фаренгейта в градусы Цельсия (предполагается, что код этого датчика недоступен для модификации). #include // Уже существующий класс температурного датчика окружающей среды class FahrenheitSensor { public: // Получить показания температуры в градусах Фаренгейта float getFahrenheitTemp() { float t = 32.0; // ... какой то код return t; } }; class Sensor { public: virtual Sensor() {} virtual float getTemperature() = 0; }; class Adapter : public Sensor { public: Adapter( FahrenheitSensor* p ) : p_fsensor(p) { } Adapter() { delete p_fsensor; } float getTemperature() { return (p_fsensor->getFahrenheitTemp()-32.0)*5.0/9.0; } private: FahrenheitSensor* p_fsensor; }; 122 int main() { Sensor* p = new Adapter( new FahrenheitSensor); cout << "Celsius temperature = " << p->getTemperature() << endl; delete p; return 0; } 12.2.2 Мост (Bridge) Мост (Bridge) шаблон проектирования, используемый в проектирова- нии программного обеспечения чтобы «разделять абстракцию и реализацию так, чтобы они могли изменяться независимо». Шаблон bridge (от англ. мост) использует инкапсуляцию, агрегирование и может использовать наследование для того, чтобы разделить ответственность между классами. Назначение: При частом изменении класса, преимущества объектно-ориентирован- ного подхода становятся очень полезными, позволяя делать изменения в программе, обладая минимальными сведениями о реализации программы. Шаблон Bridge является полезным там, где не только сам класс часто меня- ется, но и то, что класс делает. Плюсы: проще расширять систему новыми типами за счет сокращения общего числа родственных подклассов; возможность динамического изменения реализации в процессе вы- полнения программы; паттерн Bridge полностью скрывает реализацию от клиента. В случае модификации реализации пользовательский код не требует перекомпиля- ции. Применение: Когда абстракция и реализация разделены, они могут изменяться неза- висимо. Рассмотрим такую абстракцию как фигура. Существует множество типов фигур, каждая со своими свойствами и методами. Однако есть что-то, что объединяет все фигуры. Например, каждая фигура должна уметь рисо- вать себя, масштабироваться и т.п. В то же время рисование графики может отличаться в зависимости от типа ОС, или графической библиотеки. Фи- гуры должны иметь возможность рисовать себя в различных графических средах, но реализовывать в каждой фигуре все способы рисования или мо- дифицировать фигуру каждый раз при изменении способа рисования не- практично. В этом случае помогает шаблон Bridge, позволяя создавать но- 123 вые классы, которые будут реализовывать рисование в различных графиче- ских средах. При использовании такого подхода очень легко можно добав- лять, как новые фигуры, так и способы их рисования. Приведем реализацию логгера с применением паттерна Bridge: // Logger.h – Абстракция #include // Опережающее объявление class LoggerImpl; class Logger { public: Logger( LoggerImpl* p ); virtual Logger( ); virtual void log( string & str ) = 0; protected: LoggerImpl * pimpl; }; class ConsoleLogger : public Logger { public: ConsoleLogger(); void log( string & str ); }; class FileLogger : public Logger { public: FileLogger( string & file_name ); void log( string & str ); private: string file; }; class SocketLogger : public Logger { public: SocketLogger( string & remote_host, int remote_port ); void log( string & str ); private: string host; int port; }; // Logger.cpp – Абстракция #include "Logger.h" #include "LoggerImpl.h" Logger::Logger( LoggerImpl* p ) : pimpl(p) { } Logger::Logger( ) { delete pimpl; } 124 ConsoleLogger::ConsoleLogger() : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ) { } void ConsoleLogger::log( string & str ) { pimpl->console_log( str); } FileLogger::FileLogger( string & file_name ) : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ), file(file_name) { } void FileLogger::log( string & str ) { pimpl->file_log( file, str); } SocketLogger::SocketLogger( string & remote_host, int remote_port ) : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ), host(remote_host), port(remote_port) { } void SocketLogger::log( string & str ) { pimpl->socket_log( host, port, str); } // LoggerImpl.h – Реализация #include { public: virtual LoggerImpl( ) {} virtual void console_log( string & str ) = 0; virtual void file_log( string & file, string & str ) = 0; virtual void socket_log( tring & host, int port, string & str ) = 0; }; class ST_LoggerImpl : public LoggerImpl { 125 public: void console_log( string & str ); void file_log ( string & file, string & str ); void socket_log ( string & host, int port, string & str ); }; class MT_LoggerImpl : public LoggerImpl { public: void console_log( string & str ); void file_log ( string & file, string & str ); void socket_log ( string & host, int port, string & str ); }; // LoggerImpl.cpp – Реализация #include #include "LoggerImpl.h" void ST_LoggerImpl::console_log( string & str ) { cout << "Single-threaded console logger" << endl; } void ST_LoggerImpl::file_log( string & file, string & str ) { cout << "Single-threaded file logger" << endl; } void ST_LoggerImpl::socket_log( string & host, int port, string & str ) { cout << "Single-threaded socket logger" << endl; }; void MT_LoggerImpl::console_log( string & str ) { cout << "Multithreaded console logger" << endl; } void MT_LoggerImpl::file_log( string & file, string & str ) { cout << "Multithreaded file logger" << endl; } void MT_LoggerImpl::socket_log( string & host, int port, string & str ) { cout << "Multithreaded socket logger" << endl; } // Main.cpp #include #include "Logger.h" int main() { Logger * p = new FileLogger( string("log.txt")); 126 p->log( string("message")); delete p; return 0; } Отметим несколько важных моментов приведенной реализации паттерна Bridge: 1. При модификации реализации клиентский код перекомпилировать не нужно. Использование в абстракции указателя на реализацию (идиома pimpl) позволяет заменить в файле Logger.h включение include "Logger- Impl.h" на опережающее объявление class LoggerImpl . Такой прием сни- мает зависимость времени компиляции файла Logger.h (и, соответственно, использующих его файлов клиента) от файла LoggerImpl.h. 2. Пользователь класса Logger не видит никаких деталей его реализа- ции. |