Как мне реорганизовать мой код, чтобы удалить ненужные синглтоны? - PullRequest
20 голосов
/ 24 января 2009

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

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

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

Например:
В программе управления розничным магазином я использовал шаблон 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 ]

19 голосов
/ 24 января 2009

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

Взгляните на эти посты в отличном блоге по тестированию Google :

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

3 голосов
/ 05 мая 2009

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

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

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

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

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

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

3 голосов
/ 24 января 2009

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

3 голосов
/ 24 января 2009

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

2 голосов
/ 24 января 2009

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

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

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

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

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

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

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

1 голос
/ 24 января 2009

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

1 голос
/ 24 января 2009

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

1 голос
/ 24 января 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...