Как мне реорганизовать мой код для удаления ненужных синглонов?

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

Теперь, столкнувшись с некоторыми проблемами, прочитав этот вопрос SO, и особенно this сообщение в блоге, я понимаю зло, которое я принес в мир.

Итак: Как мне обойти удаление синглетонов из существующего кода?

Например:
В программе управления розничным магазином я использовал шаблон MVC. Объекты My Model описывают хранилище, пользовательский интерфейс - это View, и у меня есть набор контроллеров, которые действуют как лизатор между ними. Отлично. За исключением того, что я сделал Store в одноэлементное (поскольку приложение управляет только одним магазином за раз), я также использовал большинство моих классов Controller в одноэлементных (один основной, один menuBar, один productEditor...). Теперь большинство моих классов Controller получают доступ к другим синглонам следующим образом:

Store managedStore = Store::getInstance();
managedStore.doSomething();
managedStore.doSomethingElse();
//etc.

Должен ли я вместо этого:

  • Создайте один экземпляр каждого объекта и передайте ссылки на каждый объект, который нуждается в доступе к ним.
  • Использовать глобальные переменные?
  • Что-то еще?

Глобалы все равно будут плохими, но по крайней мере они не будут притворяться.

Я вижу # 1, что быстро приводит к ужасно раздутым вызовам конструктора:

someVar = SomeControllerClass(managedStore, menuBar, editor, sasquatch, ...)

Кто-нибудь еще прошел через это? Каков способ OO, позволяющий многим отдельным классам присоединиться к общей переменной, не будучи глобальным или одиночным?

Ответы

Ответ 1

Инъекция зависимостей - ваш друг.

Взгляните на эти сообщения в отличном блоге Google Testing:

Надеюсь, кто-то сделал рамки/контейнер DI для мира С++? Похоже, Google выпустил С++ Testing Framework и С++ Mocking Framework, что может помочь вам.

Ответ 2

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

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

Это ясно, какова стоимость использования глобальных переменных. Любая часть вашего приложения может изменить его. Следить за ошибками сложно, когда каждая строка кода является подозреваемой в расследовании.

Но как насчет стоимости НЕ использования глобальных переменных? Как и все остальное в программировании, это компромисс. Если вы избегаете использования глобальных переменных, вам придется передать эти объекты с сохранением состояния в качестве параметров функции. Кроме того, вы можете передать их конструктору и сохранить их как переменную-член. Когда у вас много таких объектов, ситуация ухудшается. Теперь вы находитесь threading. В некоторых случаях это не проблема. Если вы знаете, что только два или три функции должны обрабатывать этот объект Store с сохранением состояния, это лучшее решение.

Но на практике это не всегда так. Если каждая часть вашего приложения касается вашего Магазина, вы будете нарезать его на дюжину функций. Кроме того, некоторые из этих функций могут иметь сложную бизнес-логику. Когда вы нарушаете эту бизнес-логику с помощью вспомогательных функций, вам нужно - пролить свое состояние еще немного! Скажем, например, вы понимаете, что глубоко вложенная функция нуждается в некоторых данных конфигурации из объекта Store. Внезапно вам нужно отредактировать 3 или 4 объявления функций, чтобы включить этот параметр store. Затем вам нужно вернуться назад и добавить хранилище в качестве фактического параметра везде, где вызывается одна из этих функций. Возможно, единственное использование функции для Store - это передать ее какой-либо подфункции, которая ей нужна.

Шаблоны - это просто эмпирические правила. Используете ли вы всегда свои сигналы поворота перед изменением полосы в вашем автомобиле? Если вы обычный человек, вы обычно будете следовать этому правилу, но если вы едете в 4 часа ночи на пустом высоком пути, кто дает дерьмо, верно? Иногда это укусит вас в прикладе, но это управляемый риск.

Ответ 3

Мой способ избежать синглонов вытекает из идеи, что "глобальное приложение" не означает "глобальный VM" (т.е. static). Поэтому я представляю класс ApplicationContext, который содержит много прежних данных static singleton, которые должны быть глобальными приложениями, такими как хранилище конфигурации. Этот контекст передается во все структуры. Если вы используете любой контейнер IOC или диспетчер сервисов, вы можете использовать его для доступа к контексту.

Ответ 4

Что касается проблемы с раздутым вызовом конструктора, вы можете ввести классы параметров или методы factory, чтобы использовать эту проблему для вас.

Класс параметров перемещает некоторые данные параметров в собственный класс, например. например:

var parameterClass1 = new MenuParameter(menuBar, editor);
var parameterClass2 = new StuffParameters(sasquatch, ...);

var ctrl = new MyControllerClass(managedStore, parameterClass1, parameterClass2);

Как бы то ни было, проблема в другом месте. Вместо этого вы можете захотеть сэкономить свой конструктор. Сохраняйте только параметры, которые важны при построении/инициировании рассматриваемого класса, а остальные - с помощью методов getter/setter (или свойств, если вы делаете .NET).

A factory метод - это метод, который создает все экземпляры, которые вам нужны для класса, и имеет преимущество инкапсуляции создания указанных объектов. Они также довольно легко реорганизуются в сторону Singleton, потому что они похожи на методы getInstance, которые вы видите в шаблонах Singleton. Скажем, у нас есть следующий не-потоковый простой простой пример:

// The Rather Unfortunate Singleton Class
public class SingletonStore {
    private static SingletonStore _singleton
        = new MyUnfortunateSingleton();

    private SingletonStore() {
        // Do some privatised constructing in here...
    }

    public static SingletonStore getInstance() {
        return _singleton;
    }  

    // Some methods and stuff to be down here
}

// Usage: 
// var singleInstanceOfStore = SingletonStore.getInstance();

Легко реорганизовать это в сторону метода factory. Решение заключается в удалении статической ссылки:

public class StoreWithFactory {

    public StoreWithFactory() {
        // If the constructor is private or public doesn't matter
        // unless you do TDD, in which you need to have a public 
        // constructor to create the object so you can test it.
    }

    // The method returning an instance of Singleton is now a
    // factory method. 
    public static StoreWithFactory getInstance() {
        return new StoreWithFactory(); 
    }
}

// Usage:
// var myStore = StoreWithFactory.getInstance();

Использование по-прежнему остается прежним, но вы не увязли с одним экземпляром. Естественно, вы переместили бы этот метод factory к своему собственному классу, так как класс Store не должен касаться самого себя (и по совпадению следуйте принципу единой ответственности как эффект перемещения метода factory).

Отсюда у вас много вариантов, но я оставлю это как упражнение для себя. Здесь легко переусердствовать (или перегревать). Мой совет заключается в том, чтобы применять шаблон только тогда, когда есть потребность в нем.

Ответ 5

Это не единственная проблема, которая является проблемой. Это прекрасно, если у вас есть объект, из которого будет только один экземпляр. Проблема заключается в глобальном доступе. Ваши классы, которые используют Store, должны получить экземпляр Store в конструкторе (или иметь элемент свойств Store/data, который может быть установлен), и все они могут получить один и тот же экземпляр. В магазине может храниться логика, чтобы гарантировать, что только один экземпляр будет создан.

Ответ 6

Хорошо, прежде всего, "синглтоны всегда злы", понятие неверно. Вы используете Singleton всякий раз, когда у вас есть ресурс, который не будет или не будет дублироваться. Нет проблем.

Тем не менее, в вашем примере очевидная степень свободы в приложении: кто-то может прийти и сказать "но я хочу два магазина".

Существует несколько решений. Тот, который встречается в первую очередь, заключается в создании класса factory; когда вы запрашиваете Store, он дает вам одно имя с каким-либо универсальным именем (например, URI.) Внутри этого магазина вы должны быть уверены, что несколько копий не наступают друг на друга, через критические области или какой-либо метод обеспечивая атомарность транзакций.

Ответ 7

Miško Hevery имеет хорошую серию статей по тестируемости, среди прочего singleton, где он говорит не только о проблемах, но и о том, как вы можете его решить (см. "Исправление недостатка" ).

Ответ 8

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

class Log
{
  void logmessage(...)
  { // do some stuff
  }
};

int main()
{
  Log log;

  // do some more stuff
}

class Database
{
  Log &_log;
  Database(Log &log) : _log(log) {}
  void Open(...)
  {
    _log.logmessage(whatever);
  }
};

Использование singleton дает все возможности анти-шаблона Singleton, но делает ваш код более легко расширяемым и делает его пригодным для тестирования (в смысле слова, определенного в блоге тестирования Google). Например, мы можем решить, что нам нужна возможность одновременно регистрироваться на веб-службе, используя синглтон, мы можем легко сделать это без значительных изменений кода.

Для сравнения, шаблон Singleton является другим именем глобальной переменной. Он никогда не используется в производственном коде.