kapo4ka 27 октября 2019

Паттерны проектирования: твоя настольная статья

Зачем вам паттерны проектирования? Кратко о назначении каждого. Разберём, как отличить композицию от агрегации и что лучше: наследование или композиция. 
Паттерны проектирования: твоя настольная статья

Паттерны проектирования делятся на три группы.

1. Порождающие паттерны

1. Абстрактная фабрика помогает создавать семейства объектов с общими связями или зависимостями без указания их конкретных классов. Фабрики и продукты – основа паттерна. Как раз слово «семейства» отличает шаблон от других порождающих паттернов, где только один тип объекта.

2. Строитель отделяет конструирование сложного объекта от его представления. Представьте приготовление чая с сахаром, чая с молоком и обыкновенного чая. Вы одинаково кипятите воду и насыпаете заварку, а в зависимости от типа добавляете сахар, вливаете молоко или ничего не делаете.

  • Строитель определяет способ конструирования отдельных частей. Содержит процессы для инициализации и конфигурации продукта (чая).
  • Директор берёт эти отдельные функции и задаёт последовательность создания продукта.
  • Продукт – конечный объект, полученный при взаимодействии строителя и директора.

3. Фабричный метод определяет интерфейс для конструирования объекта и даёт свободу дочерним классам выбирать тип класса.

Паттерн делегирует создание экземпляров подклассам. Создадим полосу прокрутки в стиле Mac:

        ScrollBar *sb = new MacScrollBar;
    

А чтобы это работало на любой платформе, напишем код:

        ScrollBar *sb = guiFactory->CreateScrollBar();

    

Поскольку guiFactory – экземпляр класса MacFactory, CreateScrollBar возвращает новый экземпляр полосы прокрутки в стиле Mac. MacFactory – сам по себе дочерний класс GUIFactory – абстрактного суперкласса, определяющего общий интерфейс для виджетов.

Переменную экземпляра guiFactory инициализируем так:

        GUIFactory *guiFactory = new MacFactory; 
    

4. Прототип определяет тип объектов с применением прототипического экземпляра и создаёт новые путём его копирования.

5. Одиночка гарантирует, что у класса будет единственный экземпляр, и предоставляет глобальную точку доступа к нему.

2. Структурные паттерны

1. Адаптер конвертирует интерфейс класса в ожидаемый клиентами интерфейс. Благодаря этому классы работают вместе, несмотря на несовместимость интерфейсов.

2. Мост отделяет абстракцию объекта от его реализации.

3. Компоновщик объединяет объекты в структуры в виде дерева для представления иерархий частей и целого. Поэтому клиенты одинаково работают и с одним объектом, и с композицией объектов.

4. Декоратор добавляет обязанности к объектам динамически. Это гибкая альтернатива дочерним классам для расширения функциональности.

Например, объект TextView отображает текст в окне. Когда нужна полоса прокрутки, используйте ScrollDecorator, граница вокруг текстовой области – BorderDecorator.

5. Фасад – единственный класс, представляющий целую подсистему.

6. Приспособленец – мелкоструктурный экземпляр для облегчения и оптимизации совместного доступа.

7. Заместитель изображает и представляет другой объект.

3. Поведенческие паттерны

1. Посредник определяет упрощённую связь между классами.

2. Хранитель фиксирует и восстанавливает внутреннее состояние объекта.

3. Интерпретатор – способ добавления языковых элементов в программу.

4. Итератор – последовательный доступ к элементам коллекции.

5. Цепочка обязанностей – способ передачи запросов между цепочкой объектов.

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

7. Состояние трансформирует поведение объекта, когда он меняет своё состояние.

8. Стратегия определяет семейство алгоритмов, инкапсулирует каждый для взаимозаменяемости. Благодаря чему изменение алгоритма не подвергается влиянию клиентов, которые его используют.

Пример: Утиный класс с инкапсулированным поведением, таким как Flybehavior для полёта и Quackbehavior для кряканья.

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

  • Субъект знает наблюдателей.
  • Наблюдатель задаёт интерфейс обновления для объекта, который ждёт уведомления об изменениях в субъекте.

10. Шаблонный метод задаёт каркас алгоритма операции, делегируя некоторые шаги дочерним классам. Дочерние классы переопределяют конкретные этапы алгоритма и оставляют его структуру неизменённой.

11. Посетитель добавляет новую функциональность классу без изменений.

Композиция против агрегации

Агрегация и композиция – сильные формы ассоциации, которые описывают отношения между целым и его частями. Таким образом, вместо отношения «имеет», как у обыкновенной ассоциации, получаем отношение «это часть» или в обратном направлении – «состоит из».

Примеры такого вида связей:

  • Оркестр – Музыкант: оркестр состоит из музыкантов, или музыкант – часть оркестра.
  • Брошюра – Продукт: брошюра состоит из продуктов, или продукт – часть брошюры.
  • Здание – Комната: здание состоит из комнат, или комната – часть здания.

Композиция ещё сильнее агрегации. Для проверки типа связи используйте правило единственного использования для композиции: часть принадлежит только одному целому.

Для примера №1 и №2 музыкант или продукт принадлежит только одному оркестру или каталогу? Да! Но может ли в примере №3 комната принадлежать двум зданиям? Нет! Сработало правило единственного использования. Таким образом, отношение №3 лучше описывается композицией, чем агрегацией.

Вот другое правило для композиции: если целое уничтожено, уничтожается ли его часть?

Только для случая №3 работает дополнительное правило композиции. Если оркестр распадётся, исчезнут ли музыканты? Нет!

Пример кода: в университете масса факультетов, в каждом из которых группа профессоров. Если университет закроется, факультетов больше не будет, но профессора останутся. Следовательно, университет – композиция факультетов, а у факультетов агрегация профессоров. Кроме того, профессор может работать не только на одном факультете, но а факультет не может быть частью двух университетов.

1. Университет – факультет: композиция: собственность: уничтожаются вместе: сильное отношение «имеет».

Составной объект – владелец компонента и отвечает за создание и уничтожение частей. Компонент – часть одного составного объекта, уничтожение которого ведёт к ликвидации частей.

Композиция гарантирует инкапсуляцию, поскольку части – члены составного объекта:

        class University
{
  ...
  private:
 
    Department faculty[20];
    

2. Факультет – профессора: агрегация: нет собственности: отдельное существование: у компонента остаётся слабое отношение «имеет».

В агрегации объект содержит только ссылку или указатель на объект. Объект не отвечает за ссылку или указатель на протяжении всего жизненного цикла:

        class Department
{
  ...
  private:
    // Агрегация
    Professor* members[5];
  ...
};
    

Полный пример кода и диаграмма:

        class Professor;
 
class Department
{
  ...
  private:
    // Агрегация
    Professor* members[5];
  ...
};
 
class University
{
  ...
  private:
 
    Department faculty[20];
  ...
 
  create dept()
  {
   ....
     // Композиция
     faculty[0]= Department(....);
     faculty[1]= Department(....);
   ....   
  }
};
    
Паттерны проектирования: твоя настольная статья

В UML композиция изображается в виде закрашенного ромба и сплошной линии. Это говорит о кратности 1 или 0...1, то есть ответственность за объект несёт только один объект. Агрегация обозначается в виде пустого ромба и сплошной линии.

Наследование против композиции

Оба способа применяют для многократного использования функциональности.

Наследование

        Class Animal{};
Class Cat : public Animal{};
    

Класс Cat связывается с классом Animal наследованием, потому что Cat происходит от Animal.

Композиция

Класс Cat связывается с Animal композицией, ведь Cat содержит переменную с указателем на объект Animal. Классы подобные Cat иногда называют front-end классами, а Animalback-end классами.

        class Animal 
{};

class Cat
{
private:
	Animal *animal;
};
    

С помощью наследования реализуют один класс на базе другого. В альтернативной композиции новая функциональность получается при составлении объекта. В результате получаем сокрытие внутренних деталей объектов, чего нет в наследовании.

Минусы наследования

Хотя наследование классов облегчает модификацию, реализация подкласса становится слишком связанной с родительской. В результате любая правка в реализации родителя затрагивает подкласс.

Посмотрите на пример:

        #include <iostream>
using namespace std; 

class Animal 
{
public: 
	int makeSound() {
		cout << "Animal is making sound" << endl;
		return 1;
	}
private:
	int ntimes;
};

class Cat : public Animal
{};

int main() 
{
	Cat *cat = new Cat();
	cat->makeSound();
	return 0;
}
    

Так как Cat наследует (переиспользует) Animal, получим:

        Animal is making sound
    

Однако, если изменить метод makeSound() родительского класса, вот так:

        Sound *makeSound(int n) {
	cout << "Animal is making sound" << endl;
	return new Sound;
}
    

Подпрограмма main() также изменится, даже когда используем не Animal, а Cat.

Вот новое решение:

        #include <iostream>
using namespace std; 

class Sound{};

class Animal 
{
public: 
	Sound *makeSound() {
		cout << "Animal is making sound" << endl;
		return new Sound;
	}
};

class Cat : public Animal
{};

int main() 
{
	Cat *cat = new Cat();
	cat->makeSound();
	return 0;
}
    

Как насчёт композиции

Композиция – альтернативный способ для Cat повторно использовать Animal-реализацию makeSound(). Добавьте в Cat указатель на экземпляр Animal и определите собственный метод makeSound(), который вызывает makeSound() для Animal. Вот код:

        #include <iostream>
using namespace std; 

class Animal 
{
public: 
	int makeSound() {
		cout << "Animal is making sound" << endl;
		return 1;
	}
};

class Cat
{
private:
	Animal *animal;
public:
	int makeSound() {
		return animal->makeSound();
	}
};

int main() 
{
	Cat *cat = new Cat();
	cat->makeSound(3);
	return 0;
}
    

При использовании композиции подкласс становится front-end классом, а суперкласс – back-end классом. При наследовании подкласс автоматически наследует реализацию любого неприватного метода суперкласса, если не переопределяет его. С композицией, однако, front-end класс явно вызывает соответствующий метод back-end класса из собственной реализации метода. Этот явный вызов иногда называется переадресацией или делегированием вызова метода back-end объекту.

Композиция даёт больше инкапсуляции, чем наследование, потому что изменение back-end класса не нарушает код, полагающийся на front-end класс. При наследовании подкласс знает детали реализации родителя и своеобразно вредит инкапсуляции.

Изменение возвращаемого типа метода makeSound() в Animal из предыдущего примера не вынуждает изменять интерфейс Cat, и, следовательно, код main() не ломается.

        #include <iostream>
using namespace std; 

class Sound{};

class Animal 
{
public: 
	Sound* makeSound() {
		cout << "Animal is making sound" << endl;
		return new Sound();
	}
};

class Cat 
{
private:
	Animal *animal;
public:
	Sound* makeSound() {
		return animal->makeSound();
	}
};

int main() 
{
	Cat *cat = new Cat();
	cat->makeSound();
	return 0;
}
    

Пример показывает, что цепная реакция от изменения back-end класса останавливается на front-end классе.

Композиция помогает сохранять каждый класс инкапсулированным и ориентированным на одну задачу. Классы и иерархии классов останутся маленькими и не разрастутся до неуправляемости.

Однако в проекте на базе композиции большее количество объектов (при уменьшении классов) и глобальное поведение опирается на их взаимосвязи, а не объявляется в одном классе.

Чаще используйте композицию.

Делегирование

Делает использование композиции для переиспользования равным по мощности наследованию. В делегировании два объекта участвуют в обработке запроса. Принимающий объект поручает действия делегату, что похоже на случай, когда подкласс доверяет запросы родительскому классу.

В коде ниже, вместо наследования класса Window от Rectangle, Window переиспользует поведение Rectangle, сохраняя экземпляр Rectangle в переменной rectangle и делегируя ему поведение, специфичное для Rectangle. Таким образом, хотя Window не Rectangle, он имеет Rectangle. С наследованием у Window было бы такое поведение. Но при композиции Window делает явное перенаправление запросов экземпляру Rectangle.

        #include <iostream>
using namespace std; 

class Rectangle
{
private:
	double height, width;
public:
	Rectangle(double h, double w) {
		height = h;
		width = w;
	}
	double area() {
		cout << "Area of Rect. Window = ";
		return height*width;
	}
};

class Window 
{
public: 
	Window(Rectangle *r) : rectangle(r){}
	double area() {
		return rectangle->area();
	}
private:
	Rectangle *rectangle;
};


int main() 
{
	Window *wRect = new Window(new Rectangle(10,20));
	cout << wRect->area();

	return 0;
}
    

И выводит:

        Area of Rect. Window = 200
    

Основное преимущество делегирования в лёгкости объединения поведения во время выполнения. Например, превратим Window в круг рантайм:

        #include <iostream>
using namespace std; 

class Shape
{
public:
	virtual double area() = 0;
};

class Rectangle : public Shape
{
private:
		double height, width;
public:
	Rectangle(double h, double w) {
		height = h;
		width = w;
	}
	double area() {
		return height*width;
	}
};

class Circle : public Shape
{
private:
		double radius;
public:
	Circle(double r) {
		radius = r;
	}
	double area() {
		return 3.14*radius*radius;
	}
};

class Window 
{
public: 
	Window (Shape *s):shape(s){}
	double area() {
		return shape->area();
	}
private:
	Shape *shape;
};


int main() 
{
	Window *wRect = new Window(new Rectangle(10,20));
	Window *wCirc = new Window(new Circle(20));
	cout << "rectangular Window:" << wRect->area() << endl;
	cout << "circular Window:" << wCirc->area() << endl;
	return 0;
}
    

Выбирайте делегирование, только когда больше упрощения, чем усложнения. Динамическое и параметризованное программное обеспечение сложнее в понимании, чем статичное, и добавляется проблема неэффективности во время выполнения. Поэтому используйте делегирование с умом, например, следуя стандартным паттернам проектирования, таким как Стратегия.

Вы используете паттерны проектирования при разработке? Выбираете композицию или наследование?

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ