Найти в Дзене

Идиома NVI в C++: Объяснение для начинающих

Сегодня разберем полезный прием в C++ — NVI (Non-Virtual Interface). Звучит сложно, но на самом деле все просто. Эта штука поможет писать более аккуратный и удобный код. Представь, что ты — начальник, который дает задания сотрудникам. Ты говоришь: "Сделай отчет", но не вдаешься в детали — кто-то делает в Excel, кто-то в Word. Главное — результат. NVI работает похоже: есть общий интерфейс (публичный метод), который вызывают все, а детали реализации (виртуальные методы) скрыты внутри классов. Основная идея NVI: публичные методы делаем обычными (не виртуальными), а виртуальные методы прячем в protected или private. Представь, что ты пишешь игру. У всех персонажей есть действие "атаковать". Но перед атакой нужно проверить, есть ли у персонажа здоровье, а после атаки — показать анимацию. Если эти проверки будут в каждом классе персонажа, ты быстро запутаешься. NVI позволяет сделать так: - В базовом классе пишем общую логику (проверки, подготовку) - В дочерних классах — только специфиче

Сегодня разберем полезный прием в C++ — NVI (Non-Virtual Interface). Звучит сложно, но на самом деле все просто. Эта штука поможет писать более аккуратный и удобный код.

Представь, что ты — начальник, который дает задания сотрудникам. Ты говоришь: "Сделай отчет", но не вдаешься в детали — кто-то делает в Excel, кто-то в Word. Главное — результат.

NVI работает похоже: есть общий интерфейс (публичный метод), который вызывают все, а детали реализации (виртуальные методы) скрыты внутри классов.

Основная идея NVI: публичные методы делаем обычными (не виртуальными), а виртуальные методы прячем в protected или private.

Представь, что ты пишешь игру. У всех персонажей есть действие "атаковать". Но перед атакой нужно проверить, есть ли у персонажа здоровье, а после атаки — показать анимацию. Если эти проверки будут в каждом классе персонажа, ты быстро запутаешься.

NVI позволяет сделать так:

- В базовом классе пишем общую логику (проверки, подготовку)

- В дочерних классах — только специфические действия

Давай сразу к коду. Вот как выглядит NVI
(в целях упрощения чтения имена даны на русском)

#include <iostream>

class Персонаж {

public:

// Это публичный метод, который видит пользователь

void атаковать() {

подготовка();

нанестиУрон(); // это будет делать каждый персонаж по-своему

завершение();

}

virtual ~Персонаж() = default;

protected:

// Виртуальный метод, который будут переопределять дочерние классы

virtual void нанестиУрон() {

std::cout << "Базовый урон" << std::endl;

}

private:

void подготовка() {

std::cout << "Проверяем, жив ли персонаж..." << std::endl;

}

void завершение() {

std::cout << "Показываем анимацию атаки" << std::endl;

}

};

class Маг : public Персонаж {

protected:

void нанестиУрон() override {

std::cout << "Маг атакует огненным шаром!" << std::endl;

}

};

class Воин : public Персонаж {

protected:

void нанестиУрон() override {

std::cout << "Воин рубит мечом!" << std::endl;

}

};

int main() {

Маг гэндальф;

гэндальф.атаковать();

// Вывод:

// Проверяем, жив ли персонаж...

// Маг атакует огненным шаром!

// Показываем анимацию атаки

Воин арагорн;

арагорн.атаковать();

// Вывод:

// Проверяем, жив ли персонаж...

// Воин рубит мечом!

// Показываем анимацию атаки

return 0;

}

Что здесь происходит?

1. Персонаж::атаковать() — это публичный метод. Его можно вызвать у любого персонажа. Он не виртуальный.

2. Персонаж::нанестиУрон() — protected виртуальный метод. Его нельзя вызвать напрямую из main(), но дочерние классы могут его переопределить.

3. подготовка() и завершение() — приватные методы. Они выполняются до и после атаки автоматически.

Главный плюс: когда ты вызываешь гэндальф.атаковать(), ты гарантированно получаешь и подготовку, и завершение. Никакой маг не сможет атаковать без проверки здоровья!

Более жизненный пример: логирование

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

#include <iostream>

#include <string>

class Логгер {

public:

// Публичный интерфейс — все логируют через этот метод

void записать(const std::string& сообщение) {

добавитьВремя();

записатьСообщение(сообщение); // виртуальный вызов

добавитьКонецСтроки();

}

virtual ~Логгер() = default;

protected:

// Это будут переопределять дочерние классы

virtual void записатьСообщение(const std::string& сообщение) = 0; // чисто виртуальный

private:

void добавитьВремя() {

std::cout << "[2024-01-01 12:00] "; // в реальной жизни тут текущее время

}

void добавитьКонецСтроки() {

std::cout << std::endl;

}

};

class КонсольныйЛоггер : public Логгер {

protected:

void записатьСообщение(const std::string& сообщение) override {

std::cout << сообщение;

}

};

class ФайловыйЛоггер : public Логгер {

protected:

void записатьСообщение(const std::string& сообщение) override {

// Тут была бы запись в файл

std::cout << "(пишем в файл: " << сообщение << ")";

}

};

int main() {

КонсольныйЛоггер логгер;

логгер.записать("Программа запущена");

// Вывод: [2024-01-01 12:00] Программа запущена

return 0;

}

Что дает NVI?

1. Контроль — базовый класс решает, что делать до и после виртуального метода

2. Удобство — общий код пишется один раз, а не копируется в каждый класс

3. Безопасность — нельзя случайно забыть вызвать важные подготовительные действия

4. Гибкость — легко добавить новую логику для всех классов (например, счетчик вызовов)

Когда использовать NVI?

NVI отлично подходит если:

- У тебя есть несколько классов с похожим поведением

- Перед основным действием нужно выполнять одинаковые подготовительные шаги

- После основного действия нужно делать одинаковые завершающие действия

- Ты пишешь библиотеку или фреймворк для других программистов

Подведем итог

NVI — это простая идиома С++ и полезный практически прием.

Запомни главное правило NVI: публичные методы не виртуальные, виртуальные методы не публичные.

Это помогает писать код, который легче понимать, сложнее сломать и проще расширять.