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

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

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

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

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

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

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

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

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

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

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

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

8 ответов

Решение

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

Посмотрите на эти посты в отличном блоге Google Testing:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Фабричный метод - это метод, который создает все необходимые вам экземпляры класса и имеет преимущество инкапсуляции создания указанных объектов. Их также довольно легко изменить в направлении от 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();

Это легко изменить на заводской метод. Решение состоит в том, чтобы удалить статическую ссылку:

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();

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

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

Мне нравится поощрять использование синглетонов там, где это необходимо, не поощряя использование шаблона 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);
  }
};

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

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

У Мишко Хевери есть замечательная серия статей о тестируемости, в том числе о синглтоне, где он рассказывает не только о проблемах, но и о том, как вы можете их решить (см. "Исправление ошибки").

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

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

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

Другие вопросы по тегам