Проектирование интерфейсов на C++
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 61 из 104.
Интерфейс в C++ — это абстрактный класс, в котором все функции-члены являются чисто виртуальными. В отличие от абстрактных классов, которые могут содержать некоторую реализацию, интерфейсы определяют только то, какие операции должны существовать — они задают контракт без какого-либо поведения.
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Этот интерфейс Drawable гарантирует, что любой реализующий его класс будет иметь методы draw() и resize(), но ничего не говорит о том, как они работают. Такое разделение очень эффективно — код может зависеть от интерфейса, ничего не зная о конкретных типах.
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};
class Button : public Drawable {
std::string label;
public:
Button(std::string l) : label(l) {}
void draw() override { std::cout << "Drawing button: " << label << std::endl; }
void resize(double factor) override { /* изменить размер кнопки */ }
};Класс может реализовывать несколько интерфейсов, что позволяет создавать гибкие архитектуры, в которых объекты могут выполнять различные роли:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Интерфейсы способствуют слабой связанности — ваш код зависит от абстракций, а не от конкретных реализаций, что упрощает его расширение и тестирование.
Задание
ЛегкоДавайте создадим систему управления устройствами, которая демонстрирует, как интерфейсы определяют контракты для различных типов устройств. Вы создадите два отдельных интерфейса, которые могут реализовывать устройства, а затем создадите конкретные классы устройств, выполняющие один или оба этих контракта.
Вы организуете свой код в трех файлах:
Interfaces.h: Определите два чисто абстрактных класса (интерфейса), которые представляют различные возможности устройств:Powerable— любое устройство, которое можно включить и выключить:- Чисто виртуальный метод
powerOn() - Чисто виртуальный метод
powerOff() - Чисто виртуальный метод
getPowerStatus(), возвращающийstd::string - Виртуальный деструктор
Connectable— любое устройство, которое может подключаться к сети:- Чисто виртуальный метод
connect(const std::string& network) - Чисто виртуальный метод
disconnect() - Чисто виртуальный метод
getConnectionInfo(), возвращающийstd::string - Виртуальный деструктор
- Чисто виртуальный метод
Devices.h: Реализуйте конкретные классы устройств, использующие эти интерфейсы:Lamp— реализует толькоPowerable:- Приватный член
bool isOn(начинается сfalse) - Приватный член
std::string name - Конструктор, принимающий имя лампы
powerOn()устанавливаетisOnв true и выводит:<name>: Light turned onpowerOff()устанавливаетisOnв false и выводит:<name>: Light turned offgetPowerStatus()возвращает"ON"или"OFF"в зависимости от состояния
SmartTV— реализует какPowerable, так иConnectable:- Приватные члены:
bool isOn(начинается с false),std::string brand,std::string currentNetwork(изначально пустая строка) - Конструктор, принимающий бренд телевизора
powerOn()устанавливаетisOnв true и выводит:<brand> TV: Powered onpowerOff()устанавливаетisOnв false, очищает сеть и выводит:<brand> TV: Powered offgetPowerStatus()возвращает"ON"или"OFF"connect()сохраняет имя сети и выводит:<brand> TV: Connected to <network>disconnect()очищает сеть и выводит:<brand> TV: DisconnectedgetConnectionInfo()возвращает"Connected to <network>", если подключено, или"Not connected", если строка пуста
- Приватный член
main.cpp: Считайте три входных значения (каждое на отдельной строке):- Имя лампы
- Бренд телевизора
- Имя сети
Создайте объекты
LampиSmartTV. Продемонстрируйте, как один и тот же интерфейс может использоваться с разными устройствами:Сначала поработайте с обоими устройствами через интерфейс
Powerable. Сохраните указатели на оба устройства в массивеPowerable*, затем в цикле вызовитеpowerOn()для каждого, после чего выведите их статус в формате:Status: <powerStatus>Выведите пустую строку, затем поработайте со SmartTV через интерфейс
Connectable. Создайте указательConnectable*на ваш SmartTV, вызовитеconnect()с именем сети и выведите:Connection: <connectionInfo>Выведите еще одну пустую строку, затем выключите оба устройства через массив
Powerableи покажите их итоговый статус.
Например, при входных данных Desk Lamp, Samsung и HomeWiFi:
Desk Lamp: Light turned on
Status: ON
Samsung TV: Powered on
Status: ON
Samsung TV: Connected to HomeWiFi
Connection: Connected to HomeWiFi
Desk Lamp: Light turned off
Status: OFF
Samsung TV: Powered off
Status: OFFОбратите внимание, как SmartTV может рассматриваться либо как Powerable, либо как Connectable, в зависимости от того, какой указатель на интерфейс вы используете. Эта гибкость — сила реализации нескольких интерфейсов: ваш код может работать с любым устройством, которое выполняет необходимый контракт, не зная его конкретного типа.
Шпаргалка
Интерфейс в C++ — это абстрактный класс, в котором все функции-члены являются чисто виртуальными. Интерфейсы определяют только то, какие операции должны существовать, без какой-либо реализации:
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Классы реализуют интерфейсы, переопределяя все чисто виртуальные функции:
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};Класс может реализовывать несколько интерфейсов, используя множественное наследование:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Интерфейсы способствуют слабой связанности, позволяя коду зависеть от абстракций, а не от конкретных реализаций.
Попробуйте сами
#include <iostream>
#include <string>
#include "Devices.h"
using namespace std;
int main() {
// Чтение входных данных
string lampName;
string tvBrand;
string networkName;
getline(cin, lampName);
getline(cin, tvBrand);
getline(cin, networkName);
// TODO: Создайте объект Lamp и объект SmartTV
// TODO: Создайте массив указателей Powerable*, содержащий оба устройства
// TODO: Пройдите циклом по массиву и вызовите powerOn() для каждого устройства
// После каждого powerOn() выведите: Status: <powerStatus>
// TODO: Выведите пустую строку
// TODO: Создайте указатель Connectable* на SmartTV
// Вызовите connect() с именем сети
// Выведите: Connection: <connectionInfo>
// TODO: Выведите пустую строку
// TODO: Пройдите циклом по массиву Powerable и вызовите powerOff() для каждого устройства
// После каждого powerOff() выведите: Status: <powerStatus>
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер