Практическая польза классов-синглтонов с примером на C++

13 июня 2017

Хочется думать, что объяснять, что такое класс-синглтон, или он же паттерн «одиночка», читателям данного блога не требуется. Если вдруг это не так, вам поможет, например, соответствующая статья на Википедии. Мне же хотелось бы поделиться кое-какими мыслями касательно неоднозначного отношения к данному паттерну, следует ли его вообще использовать, и если да, то когда.

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

Однако недавно мне попался интересный контр-пример. Ситуация примерно следующая. Есть класс HttpServer с очевидным назначением. На каждое новое соединение он создает новый поток (thread) и экземпляр класса HttpWorker, обслуживающий соединение на созданном потоке. Для парсинга HTTP-заголовков использовались регулярные выражения и библиотека libpcre. Предвидя многочисленные вопросы на тему «зачем писать свой HTTP-сервер», сразу поясню, что делалось все это just for fun. (Думается, что работа не пропала даром, раз она уже породила больше одного поста.)

Так вот, в наивной реализации класс HttpWorker постоянно компилировал и освобождал регулярные выражения. Что, понятное дело, как-то расточительно, учитывая типично небольшое время жизни класса и неизменяемость регулярных выражений. Хотелось прикрутить кэш скомпилированных регулярных выражений, который использовался бы всеми экземплярами класса. Сказано — сделано.

Файл RegexCache.h:

#pragma once

#include <pcre.h>

class RegexCache {
public:
    static const RegexCache& getInstance();

    RegexCache(RegexCache const&) = delete;
    void operator=(RegexCache const&) = delete;

    ~RegexCache();
    const pcre* getHttpRequestPattern() const;
    const pcre* getHttpHeaderPattern() const;

private:
    RegexCache(); /* keep the constructor private! */
    pcre* _httpRequestPattern;
    pcre* _httpHeaderPattern;
};

Файл RegexCache.cpp:

#include <RegexCache.h>
#include <pcre.h>
#include <stdexcept>

const RegexCache& RegexCache::getInstance() {
    static RegexCache cache;
    return cache;
}

RegexCache::RegexCache() {
    /*
     * Код пропущен, так как компиляция регулярных выражений
     * отношения к делу не имеет.
     */

}

RegexCache::~RegexCache() {
    if(_httpRequestPattern != nullptr)
        pcre_free(_httpRequestPattern);

    if(_httpHeaderPattern != nullptr)
        pcre_free(_httpHeaderPattern);
}

const pcre* RegexCache::getHttpRequestPattern() const {
    return _httpRequestPattern;
}

const pcre* RegexCache::getHttpHeaderPattern() const {
    return _httpHeaderPattern;
}

Соответственно, вместо постоянной компиляции и освобождения регулярных выражений, код стал просто брать их из кэша:

auto req_re = RegexCache::getInstance().getHttpRequestPattern();

Так почему же здесь класс-синглтон не создает никаких проблем? А потому что в данном конкретном случае класс-синглтон не является глобальной переменной. В сущности он является совершенно безвредной константой, пусть и с замороченной инициализацией. В более общем случае константность синглтона может изменяться. Например, синглтон может быть переменной сразу после запуска приложения, после чего становиться константой. Не думаю, что это существенно изменит ситуацию.

Кто-то возразит, что можно обойтись и без синглтона, просто объявив в классе HttpWorker статический класс RegexCache. Но в этом случае мы потеряем гарантию того, что RegexCache существует ровно в одном экземпляре. В частности, спустя пару лет приложение может быть переписано, в результате чего по ошибке создастся второй RegexCache. Эта гарантия эффективно ничего не стоит, и лишать себя ее чисто из-за перфекционизма мне видится нецелесообразным.

Вывод — синглтоны могут быть полезны. Просто не нужно лепить их на каждый чих. Что же до ускорения кода за счет кэширования, оно составило плюс 5-6% запросов в секунду. На мой взгляд, для условного получаса работы это очень даже неплохо. Соответствующий код можно найти в данном репозитории. По крайней мере, он там точно есть в районе коммита 87c80aaf. В будущем ситуация может поменяться.

А можете ли вы описать какие-то еще сценарии, когда стоит использовать синглтоны?

Метки: .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.