- •И. А. Андрианов, д. В. Кочкин, с. Ю. Ржеуцкая
- •Учебное пособие
- •Оглавление
- •1. Основы языка 8
- •1.2.2 Простые типы данных 13
- •2. Работа с памятью 73
- •3. Основы объектно-ориентированного программирования 87
- •4.Обработка исключений 114
- •5. Шаблонные функции и классы. Библиотека стандартных шаблонов 130
- •6. Паттерны проектирования 159
- •7. Антипаттерны 211
- •9. Методы отладки и оптимизации кода 242
- •1. Основы языка
- •1.1.2 Понятие проекта
- •1.2 Простые типы данных
- •1.2.1 Понятие типа
- •1.2.2 Простые типы данных
- •1.2.3 Внутреннее представление простых типов
- •1.2.4 Ключевое слово typedef. Тип size_t
- •1.3 Константы и переменные
- •1.3.1 Литералы
- •1. Числовые константы:
- •2. Символьные константы:
- •1.3.2 Переменные
- •1.3.3 Описание переменных
- •1.4. Выражения. Преобразование типов
- •1.4.1 Операнды и операции
- •1.4.2 Приоритет операций
- •1.4.3 Преобразование типов
- •1.5 Ветвления и циклы
- •1.5.2 Циклы
- •1.6 Массивы, строки
- •1.6.1 Основные понятия
- •1.6.2 Встроенные массивы
- •1.6.3 Cтроки. Обработка строк с завершающим нулём
- •1.7 Указатели и ссылки. Связь указателей и массивов. Библиотека cstring
- •1.7.1 Понятия указателя и ссылки
- •1.7.2 Связь между массивами и указателями
- •1.7.3 Библиотека cstring
- •1.8 Использование типов vector и string
- •1.8.1 Шаблонный класс vector
- •1.8.2 Класс string
- •1.9 Структуры и объединения. Битовые поля
- •1.10.1 Понятие функции
- •1.10.2 Описание функции и прототип функции
- •1.11 Параметры функции. Способы передачи параметров
- •1.11.1 Параметры функции и глобальные переменные
- •1.11.2 Способы передачи параметров в функцию
- •1.11.3 Передача массивов в функцию
- •1.11.4 Параметры-константы
- •1.11.5 Значения параметров по умолчанию
- •1.12.1 Указатель на функцию
- •1.12.2 Функции с переменным числом параметров
- •1.12.3 Перегрузка функций
- •1.12.4 Встроенные (inline) функции
- •1.13 Рекурсивные функции
- •1.14 Пространства имён
- •1.15 Директивы препроцессора. Макросы
- •2. Работа с памятью
- •2.1 Управление выделением и освобождением памяти
- •2.1.1 Статическое и динамическое выделение памяти
- •2.1.2 Способы динамического выделения и освобождения памяти
- •2.2 Динамические структуры данных
- •2.2.1 Основные понятия
- •2.2.2 Примеры реализации динамических структур на основе указателей
- •3. Основы объектно-ориентированного программирования
- •3.1 Основные понятия ооп
- •3.2.1 Описание класса
- •3.2.2 Область видимости элементов класса. Инкапсуляция
- •3.2.3 Первые примеры
- •3.3. Конструкторы и деструкторы.
- •3.4 Указатель this
- •3.5 Перегрузка операций
- •3.6 Дружественные функции и классы
- •3.7 Статические элементы класса
- •3.8 Наследование и полиморфизм
- •3.8.1. Основные понятия
- •3.8.2 Одиночное наследование
- •3.8.3 Множественное наследование
- •3.8.4 Конструкторы и деструкторы классов-потомков
- •3.9. Полиморфизм при наследовании классов
- •3.9.1 Механизмы раннего и позднего связывания
- •3.9.2 Абстрактные классы
- •4.Обработка исключений
- •4.1 Основные понятия
- •4.2 Перехват исключений
- •4.3 Поиск обработчика исключений. Раскрутка стека.
- •4.4 Повторное возбуждение исключений
- •4.5 "Аппаратные" и "программные" исключения
- •4.6 Стандартные классы исключений
- •4.7 Спецификация исключений, возбуждаемых функцией
- •4.8 Исключения в конструкторах при наследовании
- •4.9. Исключения в деструкторах
- •5. Шаблонные функции и классы. Библиотека стандартных шаблонов
- •5.1 Шаблонные функции
- •5.2 Шаблонные классы
- •5.3 Специализация шаблонов
- •5.4 Шаблонные параметры шаблонов
- •5.5 Разработка шаблонных классов с настраиваемой функциональностью
- •5.6 Использование шаблонов для вычислений на этапе компиляции
- •5.7 Библиотека стандартных шаблонов (stl) – основные понятия
- •5.8 Последовательные контейнеры. Итераторы
- •5.9. Адаптеры контейнеров
- •5.10 Ассоциативные контейнеры
- •5.11 Алгоритмы
- •6. Паттерны проектирования
- •6.1 Порождающие шаблоны
- •6.2 Структурные шаблоны
- •6.3 Шаблоны поведения
- •6.4 Шаблон "фабричный метод" (Factory method)
- •6.5 Шаблон "одиночка" (Singleton)
- •6.6 Шаблон "итератор" (Iterator)
- •6.7 Шаблон "наблюдатель" (Observer)
- •6.8 Шаблон "пул объектов" (Object pool)
- •6.9 Шаблон "команда" (Command)
- •6. 10 Шаблон "посетитель" (Visitor)
- •6.11 Дополнительные задания
- •6.11.1 Шаблон Iterator
- •6.11.2 Шаблон Observer
- •6.11.3 Шаблоны Command и Observer
- •6.11.5 Шаблон Visitor
- •6.11.5 Разработка класса − контейнера
- •6.11.6 Оценка производительности кода
- •7. Антипаттерны
- •7.1 Программирование методом копирования и вставки (Copy-Paste Programming)
- •7.2 Спагетти-код (Spaghetti code)
- •7.3 Магические числа (Magic numbers)
- •7.4 Бездумное комментирование
- •7.5 Жесткое кодирование (Hard code)
- •7.6 Мягкое кодирование (Soft code)
- •7.7 Золотой молоток (Golden hammer)
- •7.8 Слепая вера (Blind faith)
- •7.9 Ненужная сложность (Accidental complexity)
- •7.10 Божественный объект (God Object)
- •7.11 Лодочный якорь (Boat anchor)
- •7.12 Поток лавы (Lava flow)
- •7.13 Изобретение велосипеда (Reinventing the wheel)
- •7.14 Программирование перебором (Programming by permutation)
- •8.1 Выведение типов
- •8.2 Списки инициализации
- •8.3 Улучшение процесса инициализации объектов
- •8.4 Цикл for по коллекции
- •8.5 Лямбда-функции
- •8.6 Константа нулевого указателя nullptr
- •8.7 "Умные" указатели
- •9. Методы отладки и оптимизации кода
- •9.1 Отладка кода
- •9.1.1 Основные этапы отладки
- •9.1.2 Инструменты и приёмы отладки
- •9.2 Оптимизация кода
- •9.2.1 Рекомендации по выполнению оптимизации
- •9.2.2 Методики оптимизации кода
- •Заключение
- •Библиографический список
6.4 Шаблон "фабричный метод" (Factory method)
Шаблон "фабричный метод" решает задачу создания объектов в программе, при этом тип создаваемого объекта не указывается с помощью оператора new, что делает систему гибкой к изменению типов создаваемых объектов. Данный шаблон удобно применять в том случае, когда заранее неизвестно, какой тип объектов будет создан, а также в случае, если новые типы объектов будут добавляться по мере разработки и сопровождения программы.
Фабричный метод может быть реализован двумя способами: с помощью создания отдельного класса-фабрики, которая будет отвечать за создание конкретного типа объектов, либо с помощью создания в базовом классе статического метода, который, принимая идентификатор типа объекта в виде перечисляемого типа или строки, будет возвращать указатель на экземпляр созданного класса. Статический метод во втором случае также называют обобщенным конструктором.
Рассмотрим пример кода для варианта фабричного метода с обобщенным конструктором. В следующем фрагменте создается базовый класс Shape, содержащий обобщенный конструктор − метод createShape() и подклассы Circle, Rect, Triangle, переопределяющие виртуальную функцию draw(). Идентификатор создаваемого объекта передается в обобщенный конструктор в виде строки с именем класса, который необходимо создать.
// Пример 6.1.1 - шаблон "фабричный метод" на основе обобщенного
// конструктора.
#include <iostream>
#include <vector>
#include <string>
// Базовый класс иерархии
class Shape
{
public:
virtual ~Shape() {}
virtual void draw() = 0;
// Обобщенный конструктор. В качестве параметра передается
// имя типа объекта.
static Shape* createShape(const std::string& shapeType);
};
class Circle : public Shape
{
public:
virtual ~Circle() {}
virtual void draw() { std::cout << "Circle" << std::endl; }
};
class Rect : public Shape
{
public:
virtual ~Rect() {}
virtual void draw() { std::cout << "Rect" << std::endl; }
};
class Triangle : public Shape
{
public:
virtual ~Triangle() {}
virtual void draw() { std::cout << "Triangle" << std::endl; }
};
// Реализация обобщенного конструктора. В зависимости от переданной
// строки создается экземпляр нужного класса. После введения нового
// класса в иерархию потребуется дополнить обобщенный конструктор.
Shape* Shape::createShape(const std::string& shapeType)
{
if("Circle" == shapeType) return new Circle();
if("Rect" == shapeType) return new Rect();
if("Triangle" == shapeType) return new Triangle();
return 0;
}
Далее приведен пример создания объектов с помощью фабричного метода, основанного на обобщенном конструкторе. В функции test_1() выполняется заполнение вектора объектами одного из трех типов, при этом тип объекта указывается непосредственно в коде. В функции test_2() выполняется аналогичное создание объектов с помощью фабричного метода, при этом тип объекта указывается в виде строки.
// Пример 6.1.2 - шаблон "фабричный метод".
// Тестирование, создание объектов без применения фабричного
// метода - test_1() и с применением фабричного метода - test_2()
void test_1()
{
std::vector<Shape*> sv;
for(int i = 0; i != 10; ++i)
{
int r = rand() % 3;
// Тип создаваемых объектов точно указан
if(0 == r) sv.push_back(new Circle());
if(1 == r) sv.push_back(new Rect());
if(2 == r) sv.push_back(new Triangle());
}
for(int i = 0; i != 10; ++i)
{
sv[i]->draw();
}
}
void test_2()
{
std::vector<Shape*> sv;
for(int i = 0; i != 10; ++i)
{
int r = rand() % 3;
// Тип создаваемых объектов задан в виде строки
if(0 == r) sv.push_back(Shape::createShape("Circle"));
if(1 == r) sv.push_back(Shape::createShape("Rect"));
if(2 == r) sv.push_back(Shape::createShape("Triangle"));
}
for(int i = 0; i != 10; ++i)
{
sv[i]->draw();
}
}
int main()
{
test_1();
std::cout << std::endl;
test_2();
system("pause");
}
В данном случае строка с типом объекта указывается при вызове обобщенного конструктора, однако она может быть введена пользователем или прочитана из файла. Таким образом, исключается жесткая зависимость от типа создаваемого объекта.
Результат работы программы следующий:
Triangle
Triangle
Rect
Rect
Triangle
Rect
Circle
Circle
Rect
Triangle
Triangle
Triangle
Rect
Circle
Rect
Triangle
Rect
Triangle
Circle
Circle
Рассмотренная реализация фабричного метода имеет свои недостатки:
1. Обобщенный конструктор содержит большое количество однотипных конструкций сравнения идентификаторов.
2. В статическом методе createShape() прописываются все имена подклассов, которые могут быть созданы обобщенным конструктором. То есть, в базовом классе есть информация обо всех его подклассах. Такой подход искажает принцип наследования в объектно-ориентированном программировании и делает невозможным независимое создание новых типов объектов без изменения базового класса.
Вариант фабричного метода с отдельным классом-фабрикой не имеет указанных недостатков, однако это достигается за счет большего объема программного кода.
Следующий фрагмент кода содержит базовый класс Shape и три подкласса Circle, Rect, Triangle, а также базовый класс фабрики ShapeFactory и три подкласса для создания соответствующих подклассов класса Shape. Класс ShapeFactory имеет виртуальный метод createShape().
// Пример 6.2.1 - шаблон "фабричный метод" на основе фабрик объектов.
#include <iostream>
#include <vector>
#include <string>
#include <map>
// Базовый класс иерархии объектов
class Shape
{
public:
virtual ~Shape() {}
virtual void draw() = 0;
};
class Circle : public Shape
{
public:
virtual ~Circle() {}
virtual void draw() { std::cout << "Circle" << std::endl; }
};
class Rect : public Shape
{
public:
virtual ~Rect() {}
virtual void draw() { std::cout << "Rect" << std::endl; }
};
class Triangle : public Shape
{
public:
virtual ~Triangle() {}
virtual void draw() { std::cout << "Triangle" << std::endl; }
};
// Базовый класс фабрики объектов
class ShapeFactory
{
public:
virtual ~ShapeFactory() {}
virtual Shape* createShape() = 0;
};
class CircleFactory : public ShapeFactory
{
public:
virtual Shape* createShape() { return new Circle(); }
};
class RectFactory : public ShapeFactory
{
public:
virtual Shape* createShape() { return new Rect(); }
};
class TriangleFactory : public ShapeFactory
{
public:
virtual Shape* createShape() { return new Triangle(); }
};
Класс ShapeManager может быть использован для удобства управления объектами фабрик и обеспечения централизованного доступа к созданию объектов. Данный класс содержит два метода − addFactory() и createShape(). Метод addFactory() регистрирует фабрику, присваивая ей определенное имя, метод createShape() получает имя объекта, который нужно создать, ищет фабрику с таким именем и делегирует ей запрос на создание.
// Пример 6.2.2 - шаблон "фабричный метод".
// Вспомогательный класс ShapeManager, позволяющий хранить экземпляры
// фабрик объектов, а также делегировать запросы на создание
// объектов фабрикам.
class ShapeManager
{
// Ассоциативный массив с фабриками. Тип фабрики и тип объектов
// определяется с помощью строки.
std::map<std::string, ShapeFactory*> mFactoryMap;
public:
~ShapeManager()
{
// Удаление фабрик.
for(std::map<std::string, ShapeFactory*>::iterator it =
mFactoryMap.begin();
it != mFactoryMap.end();
++it)
{
delete it->second;
}
mFactoryMap.clear();
}
void addFactory(const std::string& name, ShapeFactory* factory)
{
// Фабрика сохраняется в ассоциативном массиве
// под заданным именем
mFactoryMap.insert(std::make_pair(name, factory));
}
Shape* createShape(const std::string& shapeType)
{
// Делегирования запроса на создание объекта фабрике с
// указанным именем.
return mFactoryMap[shapeType]->createShape();
}
};
В следующем фрагменте кода приведен новый класс объектов MyShape и фабрики для создания экземпляров данного класса, а также функция test_2() для демонстрации работы фабричного метода. Код метода test_1() аналогичен коду одноименного метода из примера с обобщенным конструктором.
// Пример 6.2.3 - шаблон "фабричный метод"
// Новый класс фигур
class MyShape : public Shape
{
public:
virtual ~MyShape() {}
virtual void draw() { std::cout << "MyShape" << std::endl; }
};
// Фабрика для создания новых фигур
class MyShapeFactory : public ShapeFactory
{
public:
virtual Shape* createShape() { return new MyShape(); }
};
void test_2()
{
ShapeManager shapeManager;
// Регистрация фабрик в менеджере
shapeManager.addFactory("Circle", new CircleFactory());
shapeManager.addFactory("Rect", new RectFactory());
shapeManager.addFactory("Triangle", new TriangleFactory());
// Добавление новой фабрики
shapeManager.addFactory("MyShape", new MyShapeFactory());
std::vector<Shape*> sv;
for(int i = 0; i != 15; ++i)
{
int r = rand() % 4;
// Процесс создания объектов похож на вариант с обобщенным
// конструктором, однако, в данном примере объекты создаются
// в конкретных фабриках, а запросы на создание объектов
// направляются менеджеру.
if(0 == r) sv.push_back(shapeManager.createShape("Circle"));
if(1 == r) sv.push_back(shapeManager.createShape("Rect"));
if(2 == r) sv.push_back(shapeManager.createShape("Triangle"));
if(3 == r) sv.push_back(shapeManager.createShape("MyShape"));
}
for(int i = 0; i != 15; ++i)
{
sv[i]->draw();
}
}
int main()
{
test_1();
std::cout << std::endl;
test_2();
system("pause");
}
Результат работы программы следующий:
Triangle
Triangle
Rect
Rect
Triangle
Rect
Circle
Circle
Rect
Triangle
Rect
Rect
Rect
MyShape
Rect
MyShape
MyShape
Triangle
MyShape
Circle
MyShape
Circle
Triangle
Rect
Circle
Рассмотренный вариант фабричного метода позволяет создавать объекты без жесткого указания типа объектов, а также позволяет расширять систему за счет определения новых объектов за пределами основной программы − например, в динамической библиотеке. Для создания нового типа объектов достаточно определить сам класс объектов и класс-фабрику.
Рассмотрим в качестве дополнительного упражнения модификацию предыдущего варианта фабричного метод с шаблонным классом − фабрикой. Шаблонная фабрика позволит избежать написания однотипного кода классов фабрик.
// Пример 6.3 - шаблон "фабричный метод" на основе фабрики объектов.
// Фабрика реализована через механизм шаблонов (template) языка С++
#include <iostream>
#include <vector>
#include <string>
#include <map>
class Shape
{
public:
virtual ~Shape() {}
virtual void draw() = 0;
};
class Circle : public Shape
{
public:
virtual ~Circle() {}
virtual void draw() { std::cout << "Circle" << std::endl; }
};
class Rect : public Shape
{
public:
virtual ~Rect() {}
virtual void draw() { std::cout << "Rect" << std::endl; }
};
class Triangle : public Shape
{
public:
virtual ~Triangle() {}
virtual void draw() { std::cout << "Triangle" << std::endl; }
};
class ShapeFactoryInterface
{
public:
virtual ~ShapeFactoryInterface() {}
virtual Shape* createShape() = 0;
};
template<class TShape>
class ShapeFactory : public ShapeFactoryInterface
{
public:
virtual ~ShapeFactory() {}
virtual Shape* createShape() { return new TShape(); }
};
class ShapeManager
{
std::map<std::string, ShapeFactoryInterface*> mFactoryMap;
public:
~ShapeManager()
{
for(std::map<std::string,ShapeFactoryInterface*>::iterator it
= mFactoryMap.begin();
it != mFactoryMap.end();
++it)
{
delete it->second;
}
mFactoryMap.clear();
}
void addFactory(const std::string& name,
ShapeFactoryInterface* factory)
{
mFactoryMap.insert(std::make_pair(name, factory));
}
Shape* createShape(const std::string& shapeType)
{
return mFactoryMap[shapeType]->createShape();
}
};
void test_1()
{
std::vector<Shape*> sv;
for(int i = 0; i != 10; ++i)
{
int r = rand() % 3;
if(0 == r) sv.push_back(new Circle());
if(1 == r) sv.push_back(new Rect());
if(2 == r) sv.push_back(new Triangle());
}
for(int i = 0; i != 10; ++i)
{
sv[i]->draw();
}
}
class MyShape : public Shape
{
public:
virtual ~MyShape() {}
virtual void draw() { std::cout << "MyShape" << std::endl; }
};
void test_2()
{
ShapeManager shapeManager;
shapeManager.addFactory("Circle", new ShapeFactory<Circle>());
shapeManager.addFactory("Rect", new ShapeFactory<Rect>());
shapeManager.addFactory("Triangle", new ShapeFactory<Triangle>());
shapeManager.addFactory("MyShape", new ShapeFactory<MyShape>());
std::vector<Shape*> sv;
for(int i = 0; i != 15; ++i)
{
int r = rand() % 4;
if(0 == r) sv.push_back(shapeManager.createShape("Circle"));
if(1 == r) sv.push_back(shapeManager.createShape("Rect"));
if(2 == r)
sv.push_back(shapeManager.createShape("Triangle"));
if(3 == r) sv.push_back(shapeManager.createShape("MyShape"));
}
for(int i = 0; i != 15; ++i)
{
sv[i]->draw();
}
}
int main()
{
test_1();std::cout << std::endl;
test_2();
system("pause");
}
Результат работы программы следующий:
Triangle
Triangle
Rect
Rect
Triangle
Rect
Circle
Circle
Rect
Triangle
Rect
Rect
Rect
MyShape
Rect
MyShape
MyShape
Triangle
MyShape
Circle
MyShape
Circle
Triangle
Rect
Circle
