Паттерны проектирования
MVC (Model-View-Controller)
MVC (Model-View-Controller) — это архитектурный паттерн, который разделяет приложение на три основных компонента:
- Model (Модель): Отвечает за управление данными и бизнес-логику приложения. Модель не зависит от пользовательского интерфейса и может быть использована в различных контекстах.
- View (Вид): Отображает данные пользователю и предоставляет интерфейс для взаимодействия. Вид отвечает только за отображение данных и не содержит бизнес-логики.
- Controller (Контроллер): Обрабатывает пользовательский ввод, взаимодействует с Моделью и обновляет Вид. Контроллер служит посредником между Моделью и Видом, управляя потоком данных и обработкой событий.
Преимущества использования MVC:
- Разделение ответственности: Каждый компонент имеет четко определенную роль, что упрощает разработку и поддержку кода.
- Масштабируемость: Приложение легко расширять, так как изменения в одном компоненте не затрагивают другие.
- Тестируемость: Модель и контроллер могут быть протестированы независимо от вида, что упрощает написание модульных тестов.
- Повторное использование кода: Модель и контроллер могут быть использованы в различных видах, что повышает эффективность разработки.
Пример:
Разработать программу для управления каталогом товаров. Приложение должно позволять добавлять, удалять и просматривать товары.
#include <iostream>
#include <vector>
#include <string>
// Модель
class Product {
public:
std::string name;
double price;
int quantity;
Product(std::string n, double p, int q) : name(n), price(p), quantity(q) {}
};
class CatalogModel {
public:
void addProduct(const Product& product) {
m_products.push_back(product);
}
void removeProduct(int index) {
if (index >= 0 && index < m_products.size()) {
m_products.erase(m_products.begin() + index);
}
}
const std::vector<Product>& getProducts() const {
return m_products;
}
private:
std::vector<Product> m_products;
};
// Вид
class CatalogView {
public:
void displayProducts(const std::vector<Product>& products) {
for (size_t i = 0; i < products.size(); ++i) {
std::cout << i << ". " << products[i].name << " - $" << products[i].price
<< " - " << products[i].quantity << " шт." << std::endl;
}
}
void displayMenu() {
std::cout << "1. Добавить товар" << std::endl;
std::cout << "2. Удалить товар" << std::endl;
std::cout << "3. Показать товары" << std::endl;
std::cout << "4. Выход" << std::endl;
}
};
// Контроллер
class CatalogController {
public:
CatalogController(CatalogModel& model, CatalogView& view) : m_model(model), m_view(view) {}
void run() {
int choice;
do {
m_view.displayMenu();
std::cin >> choice;
switch (choice) {
case 1:
addProduct();
break;
case 2:
removeProduct();
break;
case 3:
m_view.displayProducts(m_model.getProducts());
break;
case 4:
std::cout << "Выход..." << std::endl;
break;
default:
std::cout << "Неверный выбор. Попробуйте снова." << std::endl;
}
} while (choice != 4);
}
private:
void addProduct() {
std::string name;
double price;
int quantity;
std::cout << "Введите название товара: ";
std::cin >> name;
std::cout << "Введите цену товара: ";
std::cin >> price;
std::cout << "Введите количество товара: ";
std::cin >> quantity;
m_model.addProduct(Product(name, price, quantity));
}
void removeProduct() {
int index;
std::cout << "Введите номер товара для удаления: ";
std::cin >> index;
m_model.removeProduct(index);
}
CatalogModel& m_model;
CatalogView& m_view;
};
int main() {
CatalogModel model;
CatalogView view;
CatalogController controller(model, view);
controller.run();
return 0;
}
MVC в Qt
Qt — это мощная кроссплатформенная библиотека для разработки приложений с графическим интерфейсом. Руководство по ее установке есть в нашем курсе. В Qt паттерн MVC можно организовать следующим образом:
- Model: Используется класс QAbstractItemModel или его подклассы (например, QStandardItemModel, QSqlTableModel). Модель предоставляет данные для отображения и управления ими.
- View: Используется класс QAbstractItemView или его подклассы (например, QListView, QTableView, QTreeView). Вид отображает данные, предоставленные моделью.
- Controller: В Qt контроллер часто объединен с виджетами, которые обрабатывают пользовательский ввод (например, кнопки, поля ввода). Контроллер взаимодействует с моделью и обновляет вид в зависимости от действий пользователя.
Ознакомиться с другими примерами можно в следующих проектах:
Задача: To-Do List
Описание предметной области:
Вам необходимо разработать масштабируемую систему управления списком задач (ToDo List), которая позволяет пользователям добавлять, удалять, отмечать как выполненные и просматривать задачи. Система должна поддерживать различные категории задач, приоритеты и сроки выполнения. Пользователи могут фильтровать задачи по категориям, приоритетам и статусу выполнения.
- Модель (Model):
- Реализуйте класс Task, который будет содержать информацию о задаче (название, описание, категория, приоритет, статус выполнения, срок выполнения).
- Создайте класс ToDoModel, который будет управлять списком задач. Предоставьте методы для добавления, удаления, изменения статуса выполнения и получения списка задач.
- Вид (View):
- Отображайте меню пользователю с возможными действиями (добавить задачу, удалить задачу, отметить задачу как выполненную, показать все задачи).
- Отображайте список задач с их атрибутами.
- Контроллер (Controller):
- Обрабатывайте пользовательский ввод.
- Взаимодействуйте с Моделью для выполнения действий (добавление, удаление, изменение статуса).
- Обновляйте Вид в зависимости от результатов действий.
Для реализации доступно два варианта: простой консольный и графический при помощи Qt, за второй будут дополнительные баллы. Вместе с Qt можно использовать Qml.
Доп. функции:
- Реализуйте возможность экспорта списка задач в файл (например, в формате CSV или JSON).
Factory Method (Фабричный метод)
Паттерн Фабричный Метод определяет интерфейс создания объекта, но позволяет субклассам выбрать класс создаваемого экемпляра. Таким образом, Фабричный Метод делегирует операцию создания экземпляра субклассам.
Когда стоит использовать?
Представьте, что вы разрабатываете систему логистики, в которой задействованы разные типы транспорта, например, грузовики и корабли. Добавление нового типа транспорта (например, самолетов) требует изменения существующего кода, что может привести к ошибкам. Паттерн Factory Method помогает решить эту проблему, инкапсулируя логику создания объектов внутри подклассов.
Компоненты
- Продукт (Product): Определяет общий интерфейс для всех создаваемых объектов.
- Конкретный продукт (Concrete Product): Реализует интерфейс продукта.
- Создатель (Creator): Определяет фабричный метод, который возвращает объекты продукта.
- Конкретный создатель (Concrete Creator): Реализует фабричный метод, создавая определённый тип продукта.
Пример:
Рассмотрим пример с системой доставки.
#include <iostream>
#include <memory>
#include <string>
// Продукт
class Transport {
public:
virtual ~Transport() = default;
virtual void deliver() const = 0;
};
// Конкретные продукты
class Truck : public Transport {
public:
void deliver() const override {
std::cout << "Доставка по суше грузовиком." << std::endl;
}
};
class Ship : public Transport {
public:
void deliver() const override {
std::cout << "Доставка по морю кораблем." << std::endl;
}
};
// Создатель
class Logistics {
public:
virtual ~Logistics() = default;
virtual std::unique_ptr<Transport> createTransport() const = 0;
void planDelivery() const {
auto transport = createTransport();
transport->deliver();
}
};
// Конкретные создатели
class RoadLogistics : public Logistics {
public:
std::unique_ptr<Transport> createTransport() const override {
return std::make_unique<Truck>();
}
};
class SeaLogistics : public Logistics {
public:
std::unique_ptr<Transport> createTransport() const override {
return std::make_unique<Ship>();
}
};
// Клиентский код
int main() {
std::unique_ptr<Logistics> logistics = std::make_unique<RoadLogistics>();
logistics->planDelivery();
logistics = std::make_unique<SeaLogistics>();
logistics->planDelivery();
return 0;
}
Преимущества и недостатки
Преимущества:
- Инкапсуляция: Логика создания объектов скрыта от клиентского кода.
- Масштабируемость: Легко добавить новый тип продукта, создав новый подкласс.
- Гибкость: Позволяет переключаться между разными типами объектов в зависимости от контекста.
Недостатки:
- Сложность: Введение дополнительного уровня абстракции может усложнить архитектуру.
- Множество подклассов: При добавлении большого числа продуктов требуется создавать множество новых классов.
Применение на практике
Если ваша система нуждается в создании объектов с общим интерфейсом, но с различным поведением, Factory Method станет полезным инструментом. Например:
- Логистика: Доставка грузов с использованием разных видов транспорта.
- Игры: Создание разных типов врагов в зависимости от уровня.
- Документы: Генерация отчетов в различных форматах (PDF, Word).
Задание: Система управления парком транспортных средств
Описание предметной области:
Вам необходимо разработать систему, которая поддерживает управление различными типами транспортных средств (например, грузовики, корабли, самолеты). Система должна позволять планировать доставку с использованием выбранного типа транспорта. Каждое транспортное средство должно иметь свои особенности, такие как способ доставки, стоимость доставки и ограничения по грузу. Система должна быть масштабируемой, чтобы в будущем можно было добавлять новые виды транспорта (например, дроны).
Задание:
- Реализуйте базовый класс
Transport
, который содержит информацию о транспортном средстве (например, название, максимальная грузоподъемность).- Определите метод
deliver
, который будет переопределяться в конкретных классах.
- Определите метод
- Создайте производные классы для конкретных видов транспорта:
Truck
для грузовиков (доставка по суше).Ship
для кораблей (доставка по морю).Plane
для самолетов (доставка по воздуху).
- Реализуйте базовый класс
Logistics
с фабричным методом для создания транспортных средств.- Определите метод
createTransport
, который будет реализован в подклассах.
- Определите метод
- Создайте конкретные классы логистики:
RoadLogistics
для грузовиков.SeaLogistics
для кораблей.AirLogistics
для самолетов.
- Реализуйте возможность расчета стоимости доставки на основе типа транспорта и расстояния.
Доп. функции:
- Добавьте поддержку нового типа транспорта (например, дронов) с собственными характеристиками.
- Реализуйте систему отчётов, которая выводит информацию о выполненных доставках (тип транспорта, расстояние, стоимость).
Входные данные для тестирования:
Доступные типы транспорта:
1. Грузовик (Truck) - Макс. груз: 10 тонн
2. Корабль (Ship) - Макс. груз: 100 тонн
3. Самолет (Plane) - Макс. груз: 20 тонн
Запросы на доставку:
1. Тип транспорта: Грузовик, расстояние: 300 км, груз: 5 тонн.
2. Тип транспорта: Корабль, расстояние: 1500 км, груз: 50 тонн.
3. Тип транспорта: Самолет, расстояние: 2000 км, груз: 15 тонн.
Выходные данные:
Доставка Грузовиком
Расстояние: 300 км
Груз: 5 тонн
Стоимость: 1500 руб
Доставка Кораблём
Расстояние: 1500 км
Груз: 50 тонн
Стоимость: 7500 руб
Доставка Самолётом
Расстояние: 2000 км
Груз: 15 тонн
Стоимость: 20000 руб
Отчёт о доставках:
1. Грузовик: 300 км, 5 тонн, 1500 руб
2. Корабль: 1500 км, 50 тонн, 7500 руб
3. Самолёт: 2000 км, 15 тонн, 20000 руб
Adapter (Адаптер)
Паттерн Адаптер используется для преобразования интерфейса одного класса в интерфейс, ожидаемый клиентом. Адаптер позволяет классам с несовместимыми интерфейсами работать вместе.
Когда стоит использовать?
Представьте, что у вас есть система управления платёжными сервисами. Система уже работает с одним API, но вам нужно интегрировать сторонний платёжный сервис с другим интерфейсом. Изменение существующего кода может быть сложным или нежелательным. Паттерн Adapter позволяет решить эту проблему, создавая промежуточный слой, который преобразует интерфейсы.
Компоненты
- Клиент (Client): Класс, использующий целевой интерфейс.
- Целевой интерфейс (Target): Интерфейс, ожидаемый клиентом.
- Адаптируемый класс (Adaptee): Класс с несовместимым интерфейсом, который нужно адаптировать.
- Адаптер (Adapter): Класс, который реализует целевой интерфейс и использует адаптируемый класс для выполнения своей работы.
Пример
Рассмотрим пример с платёжными системами.
#include <iostream>
#include <string>
// Целевой интерфейс (Target)
class PaymentProcessor {
public:
virtual ~PaymentProcessor() = default;
virtual void processPayment(double amount) const = 0;
};
// Адаптируемый класс (Adaptee)
class OldPaymentSystem {
public:
void makeTransaction(const std::string& details) const {
std::cout << "Processing payment with details: " << details << std::endl;
}
};
// Адаптер (Adapter)
class PaymentAdapter : public PaymentProcessor {
private:
OldPaymentSystem* adaptee;
public:
PaymentAdapter(OldPaymentSystem* oldSystem) : adaptee(oldSystem) {}
void processPayment(double amount) const override {
// Преобразуем данные в формат, понятный старой системе
std::string details = "Amount: " + std::to_string(amount);
adaptee->makeTransaction(details);
}
};
// Клиент (Client)
class OnlineStore {
private:
const PaymentProcessor* paymentProcessor;
public:
OnlineStore(const PaymentProcessor* processor) : paymentProcessor(processor) {}
void checkout(double amount) const {
std::cout << "Starting payment process...\n";
paymentProcessor->processPayment(amount);
std::cout << "Payment complete.\n";
}
};
// Клиентский код
int main() {
OldPaymentSystem oldSystem;
PaymentAdapter adapter(&oldSystem);
OnlineStore store(&adapter);
store.checkout(99.99);
return 0;
}
Преимущества и недостатки
Преимущества:
- Совместимость: Позволяет использовать несовместимые классы без изменения их кода.
- Инкапсуляция: Изменения в адаптируемом классе не влияют на клиентский код.
- Гибкость: Позволяет легко подключать новые адаптируемые классы.
Недостатки:
- Сложность: Увеличивает сложность системы из-за добавления новых классов.
- Зависимость: Адаптер может стать слишком завязанным на структуру адаптируемого класса.
Применение на практике
Если ваша система работает с объектами, интерфейсы которых несовместимы, Adapter поможет их объединить. Например:
- Платёжные системы: Интеграция старого и нового API.
- Файловые системы: Работа с разными форматами файлов.
- Сетевые протоколы: Преобразование форматов данных для совместимости.
Задание: Система работы с данными GPS-устройств
Описание предметной области:
У вас есть система, которая работает с GPS-данными через современный интерфейс GeoTarget
. В проекте появилась необходимость интегрировать старую библиотеку LegacyGPS
, предоставляющую данные в ином формате. Необходимо разработать адаптер для взаимодействия с этой библиотекой, чтобы клиентский код не изменялся.
Инструкции:
- Создайте интерфейс
GeoTarget
с методомgetCoordinates()
, возвращающим координаты в формате(широта, долгота)
. - Реализуйте класс
LegacyGPS
, который предоставляет координаты в виде строки ("lat:<широта>;lon:<долгота>"
). - Создайте адаптер
GPSAdapter
, который преобразует данные из форматаLegacyGPS
в форматGeoTarget
. - Напишите клиентский класс
GeoClient
, который работает только с интерфейсомGeoTarget
. - Напишите клиентский код, использующий адаптер для работы со старой библиотекой
LegacyGPS
.
Входные данные для тестирования:
Данные от LegacyGPS:
"lat:59.9342802;lon:30.3350986"
Выходные данные:
Координаты:
Широта: 59.9342802
Долгота: 30.3350986
Observer (Наблюдатель)
Паттерн Наблюдатель определяет отношение "один ко многим" между объектами таким образом, что при изменении состояния одного объекта происходит автоматическое оповещение и обновление всех зависимых объектов.
Когда стоит использовать?
Представьте, что у вас есть система мониторинга, которая отслеживает состояние различных датчиков. Когда датчик изменяет свои показания, система должна уведомить все заинтересованные объекты (например, пользователей или другие системы). Вместо того чтобы напрямую обновлять каждый объект, паттерн Observer позволяет оповестить все наблюдатели о изменении.
Компоненты
- Наблюдаемый объект (Subject): Объект, за состоянием которого следят наблюдатели.
- Наблюдатель (Observer): Объект, который реагирует на изменения в наблюдаемом объекте.
- Конкретный наблюдаемый объект (ConcreteSubject): Реализация наблюдаемого объекта.
- Конкретный наблюдатель (ConcreteObserver): Реализация наблюдателя.
Пример
Рассмотрим пример с системой мониторинга погоды.
#include <iostream>
#include <vector>
#include <string>
// Абстрактный наблюдатель (Observer)
class Observer {
public:
virtual ~Observer() = default;
virtual void update(float temperature) = 0;
};
// Субъект (Subject)
class Subject {
public:
virtual ~Subject() = default;
virtual void addObserver(Observer* observer) = 0;
virtual void removeObserver(Observer* observer) = 0;
virtual void notifyObservers() = 0;
};
// Конкретный субъект (ConcreteSubject)
class WeatherStation : public Subject {
private:
std::vector<Observer*> observers;
float temperature;
public:
WeatherStation() : temperature(0.0f) {}
void setTemperature(float temp) {
temperature = temp;
notifyObservers();
}
float getTemperature() const {
return temperature;
}
void addObserver(Observer* observer) override {
observers.push_back(observer);
}
void removeObserver(Observer* observer) override {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
void notifyObservers() override {
for (Observer* observer : observers) {
observer->update(temperature);
}
}
};
// Конкретный наблюдатель (ConcreteObserver)
class DisplayUnit : public Observer {
private:
std::string name;
public:
DisplayUnit(const std::string& unitName) : name(unitName) {}
void update(float temperature) override {
std::cout << name << " received temperature update: " << temperature << "°C\n";
}
};
// Клиентский код
int main() {
WeatherStation weatherStation;
DisplayUnit display1("Display 1");
DisplayUnit display2("Display 2");
weatherStation.addObserver(&display1);
weatherStation.addObserver(&display2);
weatherStation.setTemperature(25.0f); // Обновление температуры
weatherStation.removeObserver(&display2);
weatherStation.setTemperature(30.0f); // Обновление температуры
return 0;
}
Преимущества и недостатки
Преимущества:
- Декуплирование: Наблюдатели и субъекты не зависят друг от друга напрямую, что упрощает расширение системы.
- Гибкость: Легко добавлять новых наблюдателей без изменения кода субъекта.
- Автоматическое обновление: Все наблюдатели получают уведомления о изменениях состояния без явного запроса.
Недостатки:
- Производительность: Если наблюдателей много, обновления могут повлиять на производительность.
- Сложность управления: В сложных системах может быть трудно отслеживать все взаимодействия между субъектами и наблюдателями.
Применение на практике
Паттерн Observer полезен, когда:
- Необходимо уведомлять несколько объектов о изменениях состояния одного объекта.
- Система имеет динамическое добавление/удаление компонентов, которые должны быть уведомлены.
- Например:
- Системы мониторинга: Обновления данных о погоде, изменениях состояния оборудования.
- UI-системы: Интерфейсы, где несколько элементов могут изменяться на основе одного состояния (например, изменение данных, отображаемых в нескольких местах).
- Системы событий: Регистрация и уведомление об изменениях в системе.
Задание: Система уведомлений о новых сообщениях
Описание предметной области:
У вас есть система уведомлений, которая сообщает пользователям о новых сообщениях в чате. Когда новое сообщение появляется в чате, все подписанные пользователи должны получить уведомление. Для реализации этой системы нужно использовать паттерн Наблюдатель.
Инструкции:
- Создайте абстрактный класс
Observer
с методомupdate()
, который будет получать уведомления о новых сообщениях. - Реализуйте класс
Subject
для управления списком пользователей, которые подписаны на уведомления. - Создайте класс
ChatRoom
, который будет наследоватьSubject
и реализовывать логику отправки уведомлений всем подписанным пользователям о новых сообщениях. - Реализуйте класс
User
, который будет наследоватьObserver
и отображать полученные сообщения. - Напишите код, который будет добавлять пользователей в чат и отправлять новое сообщение, чтобы все подписанные пользователи получили уведомления.
Входные данные для тестирования:
Пользователь "Alice" подписан на уведомления.
Пользователь "Bob" подписан на уведомления.
Новое сообщение: "Привет, мир!"
Пользователь "Alice" получил новое сообщение: Привет, мир!
Пользователь "Bob" получил новое сообщение: Привет, мир!