Класс C ++ Singleton - передовая практика наследования

В существующем проекте я должен унаследовать класс контроллера (MVC), объявленный как Singleton, чтобы определить свое собственное лечение. Как правильно наследовать этот класс Singleton?

Во-первых, я подробно остановлюсь на контексте и необходимости этого наследования.

Приложение, которое я добавляю к существующему программному обеспечению, хочет использовать модуль MVC, который выполняет почти ту же задачу, что и я. Он использует те же методы, вплоть до подписи, с небольшими изменениями. Переписывание моего собственного модуля MVC определенно будет дублированием кода. Существующий модуль внутренне ориентирован на его применение в другой части программного обеспечения, и я не могу просто использовать тот же модуль. Но он написан как шаблон Модель-Представление-Контроллер, где Контроллер - Синглтон. Я получил View уже.

Во-вторых, я сомневаюсь, что смогу классически вывести класс Singleton.

Вызов конструктора из унаследованного класса просто вызовет getinstance () для родительского класса и не сможет вернуть объект из производного класса (?).

В-третьих, я вижу способ справиться с этим. Прокомментируйте / помогите мне улучшить!

Я копирую весь класс Singleton в класс, который я мог бы назвать AbstractController. Я дважды получаю этот класс. Первый дочерний элемент является одноэлементным и принимает все методы родительского класса. Второй дочерний элемент - это Контроллер для моей части приложения с собственной переопределенной обработкой.

Спасибо!


person kiriloff    schedule 21.02.2012    source источник
comment
По большей части, если у класса есть базовый класс, который является одноэлементным, это совершенно бесполезно: вы никогда не сможете создавать какие-либо экземпляры своего производного класса, потому что каждый такой экземпляр также будет экземпляром базового класса, а вы не разрешено делать еще один из них. (Я могу представить себе разумные исключения, но только в том случае, если базовый класс был разработан специально с мыслью о его производном ...)   -  person    schedule 21.02.2012


Ответы (2)


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

Но поскольку вы упомянули «хорошую практику», при чтении вопроса на ум приходят некоторые общие моменты:

  1. Наследование обычно не лучший инструмент для повторного использования кода. См .: Предпочитать композицию наследованию?

  2. Использование синглтона и "хорошей практики" обычно несовместимо! См .: Что плохого в синглтонах?

Надеюсь, это поможет.

person je4d    schedule 21.02.2012

По правде говоря, одиночки и наследование плохо сочетаются друг с другом.

Да, да, любители синглтонов и культ GoF будут повсюду со мной за это, говоря «ну, если вы сделаете свой конструктор защищенным ...» и «у вас нет , чтобы иметь getInstance метод класса, вы можете указать его ... ", но они просто подтверждают мою точку зрения. Синглтоны должны пройти через ряд обручей, чтобы стать одновременно синглтоном и базовым классом.

Но просто чтобы ответить на вопрос, предположим, что у нас есть одноэлементный базовый класс. Он может даже до некоторой степени усилить свою единственность посредством наследования. (Конструктор выполняет одну из немногих вещей, которые могут работать, когда он больше не может быть частным: он выдает исключение, если другой Base уже существует.) Скажем, у нас также есть класс Derived, который наследуется от Base. Поскольку мы разрешаем наследование, предположим также, что может быть любое количество других подклассов Base, которые могут наследовать или не наследовать Derived.

Но есть проблема - та самая, с которой вы либо уже столкнетесь, либо скоро столкнетесь. Если мы вызовем Base::getInstance, еще не построив объект, мы получим нулевой указатель. Мы хотели бы вернуть любой существующий одноэлементный объект (это может быть Base, и / или Derived, и / или Other). Но это сложно сделать и при этом соблюдать все правила, потому что есть всего несколько способов сделать это - и все они имеют свои недостатки.

  • Мы могли бы просто создать Base и вернуть его. Винт Derived и Other. Конечный результат: Base::getInstance() всегда возвращает ровно Base. Детские классы никогда не играют. Типа побеждает цель, ИМО.

  • Мы могли бы поместить собственное getInstance в наш производный класс и попросить вызывающего абонента сказать Derived::getInstance(), если им конкретно нужен Derived. Это значительно увеличивает взаимосвязь (потому что теперь вызывающий абонент должен знать, что нужно специально запрашивать Derived, и в конечном итоге привязывает себя к этой реализации).

  • Мы могли бы сделать вариант последнего, но вместо получения экземпляра функция просто создает его. (Пока мы это делаем, давайте переименуем функцию в initInstance, поскольку нас не особо заботит, что она получит - мы просто вызываем ее, чтобы она создавала новый Derived и устанавливала его как Единственный истинный экземпляр.)

Итак (за исключением каких-либо еще не учтенных странностей), это работает примерно так ...

class Base {
    static Base * theOneTrueInstance;

  public:
    static Base & getInstance() {
        if (!theOneTrueInstance) initInstance();
        return *theOneTrueInstance;
    }
    static void initInstance() { new Base; }

  protected:
    Base() {
         if (theOneTrueInstance) throw std::logic_error("Instance already exists");
         theOneTrueInstance = this;
    }

    virtual ~Base() { } // so random strangers can't delete me
};

Base* Base::theOneTrueInstance = 0;


class Derived : public Base {
  public:
    static void initInstance() {
        new Derived;  // Derived() calls Base(), which sets this as "the instance"
    }

  protected:
    Derived() { }   // so we can't be instantiated by outsiders
    ~Derived() { }  // so random strangers can't delete me
};

И в вашем коде инициализации вы говорите Base::initInstance(); или Derived::initInstance();, в зависимости от того, какой тип вы хотите, чтобы синглтон был. Конечно, вам нужно будет преобразовать возвращаемое значение из Base::getInstance(), чтобы использовать какие-либо Derived-специфические функции, но без преобразования вы можете использовать любые функции, определенные Base, включая виртуальные функции, переопределенные Derived.

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

  • Это возлагает большую часть бремени обеспечения единства на базовый класс. Если база не имеет этой или аналогичной функции, и вы не можете ее изменить, вы в некотором роде облажались.

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

  • Поскольку мы используем защищенные деструкторы, чтобы предотвратить удаление нашего экземпляра каким-то случайным чмо, если компилятор не умнее, чем я боюсь, даже среда выполнения не сможет правильно удалить ваш экземпляр по завершении программы. Пока, RAII ... привет предупреждения об обнаружении утечки памяти. (Конечно, память в конечном итоге будет освобождена любой приличной ОС. Но если деструктор не запускается, вы не можете зависеть от него, чтобы очистить за вас. Вам нужно будет вызвать какую-то функцию очистки, прежде чем вы exit, и это не даст вам никаких гарантий, которые дает RAII.)

  • Он предоставляет initInstance метод, который, IMO, на самом деле не относится к API, который может видеть каждый. Если вы хотите, вы можете сделать initInstance приватным и позволить вашей функции init быть friend, но тогда ваш класс делает предположения о внешнем коде, и связка возвращается в игру.

Также обратите внимание, что приведенный выше код не является потокобезопасным. Если вам это нужно, вы сами по себе.

Серьезно, менее болезненный путь - это забыть о попытках навязать одиночество. Наименее сложный способ обеспечить наличие только одного экземпляра - это создать только один. Если вам нужно использовать его в нескольких местах, рассмотрите возможность внедрения зависимостей. (Версия без фреймворка означает «передать объект тому, что в нем нуждается».: P) Я пошел и разработал вышеупомянутый материал, чтобы попытаться доказать, что я ошибаюсь в отношении синглтонов и наследования, и просто подтвердил себе, насколько зло комбинация. Я бы не рекомендовал когда-либо делать это в реальном коде.

person cHao    schedule 21.02.2012
comment
Это отличный ответ, он тщательно детализирован, содержит красочный язык и юмор. Удивительно, что он пролетел без внимания ТАК. - person ATL_DEV; 13.03.2017
comment
Это возлагает большую часть бремени обеспечения единства на базовый класс. - В некоторых случаях это может быть именно то, что мы хотим. Скажем, мы открываем экземпляр базового класса как API для внешнего мира и скрываем производный класс, на который может ссылаться только внутренний код. - person Shibir Basak; 16.05.2019
comment
@ShibirBasak: Проблема в том, что это не совсем то, что вам нужно. Это вызывает больше проблем, чем может решить. Например, только потому, что это не может быть всем. Обе стороны должны просто так сыграть свою роль, иначе вы потеряете все гарантии, которые предлагает шаблон Singleton. И как только вы будете готовы отказаться от них, вы, в первую очередь, выбросите причину, по которой следует следовать шаблону. - person cHao; 29.05.2019