Menu
Coddy logo textTech

Паттерн Singleton

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 91 из 104.

Паттерн Singleton (Одиночка) гарантирует, что у класса есть только один экземпляр на протяжении всей работы программы, и предоставляет к нему глобальную точку доступа. Это полезно для ресурсов, которые должны быть общими, таких как менеджер конфигурации, логгер или пул соединений с базой данных.

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

#include <iostream>
#include <string>

class Logger {
private:
    std::string logFile;
    
    // Private constructor - prevents direct instantiation
    // Приватный конструктор — предотвращает прямое создание экземпляра
    Logger() : logFile("app.log") {}
    
    // Delete copy constructor and assignment operator
    // Удаляем конструктор копирования и оператор присваивания
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

public:
    // Static method to access the single instance
    // Статический метод для доступа к единственному экземпляру
    static Logger& getInstance() {
        static Logger instance;  // Created once, lives until program ends
        // Создается один раз, живет до завершения программы
        return instance;
    }
    
    void log(const std::string& message) {
        std::cout << "[LOG] " << message << "\n";
    }
};

int main() {
    Logger::getInstance().log("Application started");
    
    Logger& logger = Logger::getInstance();
    logger.log("Processing data");
}

static Logger instance внутри getInstance() — это возможность C++11, называемая «magic statics» (магические статические переменные): она потокобезопасна и гарантированно инициализируется только один раз. Удаляя конструктор копирования и оператор присваивания, мы предотвращаем создание копий нашего синглтона.

Используйте Singleton с осторожностью — он вводит глобальное состояние, которое может усложнить тестирование. Он лучше всего подходит для действительно общих ресурсов, где наличие нескольких экземпляров может привести к проблемам.

challenge icon

Задание

Легко

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

Вы распределите свой код по трем файлам:

  • GameSettings.h: Определите свой класс-синглтон, который управляет конфигурацией игры.

    Ваш класс GameSettings должен хранить три настройки в качестве приватных членов: difficulty (строка), volume (целое число от 0 до 100) и fullscreen (логическое значение). Инициализируйте их значениями по умолчанию: "Normal", 50 и false.

    Реализуйте паттерн Синглтон правильно:

    • Сделайте конструктор приватным
    • Удалите конструктор копирования и оператор присваивания
    • Предоставьте статический метод getInstance(), который возвращает ссылку на единственный экземпляр

    Добавьте эти публичные методы для взаимодействия с настройками:

    • setDifficulty(const std::string& diff) — обновляет сложность
    • setVolume(int vol) — обновляет громкость
    • setFullscreen(bool fs) — обновляет полноэкранный режим
    • displaySettings() — выводит все текущие настройки
  • GameModule.h: Создайте отдельный модуль, который обращается к синглтону.

    Определите функцию с именем applyGamePreset, которая принимает название пресета (строку). Эта функция должна обращаться к синглтону GameSettings и настраивать его на основе пресета:

    • "casual" — устанавливает сложность в "Easy", громкость в 70, полноэкранный режим в false
    • "competitive" — устанавливает сложность в "Hard", громкость в 30, полноэкранный режим в true
    • Любой другой пресет — оставляет настройки по умолчанию без изменений

    Это демонстрирует, как разные части программы могут получать доступ к одному и тому же экземпляру синглтона и изменять его.

  • main.cpp: Соберите все вместе, чтобы показать синглтон в действии.

    Прочитайте два входных значения:

    1. Название пресета (строка)
    2. Пользовательский уровень громкости (целое число)

    Сначала отобразите начальные настройки по умолчанию, вызвав displaySettings() у синглтона. После этого выведите пустую строку.

    Затем примените пресет с помощью функции applyGamePreset и снова отобразите настройки. После этого выведите пустую строку.

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

Метод displaySettings() должен выводить данные именно в таком формате:

Difficulty: [value]
Volume: [value]
Fullscreen: [yes/no]

Например, при входных данных casual и 85:

Difficulty: Normal
Volume: 50
Fullscreen: no

Difficulty: Easy
Volume: 70
Fullscreen: no

Difficulty: Easy
Volume: 85
Fullscreen: no

При входных данных competitive и 20:

Difficulty: Normal
Volume: 50
Fullscreen: no

Difficulty: Hard
Volume: 30
Fullscreen: yes

Difficulty: Hard
Volume: 20
Fullscreen: yes

Обратите внимание, как и main.cpp, и GameModule.h обращаются к одному и тому же экземпляру синглтона — изменения, внесенные в одном месте, видны повсюду. В этом и заключается сила паттерна Синглтон для общих ресурсов, таких как настройки игры.

Шпаргалка

Паттерн Singleton (Одиночка) гарантирует, что у класса есть только один экземпляр на протяжении всей программы, и предоставляет к нему глобальную точку доступа. Он полезен для общих ресурсов, таких как менеджеры конфигурации, логгеры или пулы соединений с базой данных.

Ключевые шаги реализации:

  • Сделайте конструктор приватным, чтобы предотвратить прямое создание экземпляров
  • Удалите конструктор копирования и оператор присваивания, чтобы предотвратить копирование
  • Предоставьте статический метод, который возвращает ссылку на единственный экземпляр
class Logger {
private:
    std::string logFile;
    
    // Приватный конструктор
    Logger() : logFile("app.log") {}
    
    // Удаляем конструктор копирования и оператор присваивания
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

public:
    // Статический метод для доступа к единственному экземпляру
    static Logger& getInstance() {
        static Logger instance;  // Создается один раз, живет до завершения программы
        return instance;
    }
    
    void log(const std::string& message) {
        std::cout << "[LOG] " << message << "\n";
    }
};

// Использование
Logger::getInstance().log("Application started");

Logger& logger = Logger::getInstance();
logger.log("Processing data");

Строка static Logger instance внутри getInstance() использует «магические статики» C++11 — это потокобезопасно и гарантирует инициализацию только один раз.

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

Попробуйте сами

#include <iostream>
#include <string>
#include "GameSettings.h"
#include "GameModule.h"

using namespace std;

int main() {
    // Чтение входных данных
    string presetName;
    int customVolume;
    cin >> presetName;
    cin >> customVolume;
    
    // TODO: Получить экземпляр синглтона `GameSettings`
    
    // TODO: Отобразить начальные настройки по умолчанию
    
    // TODO: Вывести пустую строку
    
    // TODO: Применить пресет с помощью функции `applyGamePreset`
    
    // TODO: Отобразить настройки после применения пресета
    
    // TODO: Вывести пустую строку
    
    // TODO: Переопределить громкость с помощью `customVolume`
    
    // TODO: Отобразить итоговые настройки
    
    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование