Как мне провести рефакторинг моего кода, чтобы удалить ненужные синглтоны?

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

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

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

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

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

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

  1. Create one instance of each object and pass references to every object that needs access to them?
  2. Use globals?
  3. Something else?

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

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

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

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


person e.James    schedule 23.01.2009    source источник
comment
Не могли бы вы подробнее рассказать о том, с какими проблемами вы сталкиваетесь?   -  person Otávio Décio    schedule 24.01.2009
comment
Да, я бы хотел увидеть более конкретный пример того, что вы делаете в Singletons. Может, это все-таки не проблема?   -  person Outlaw Programmer    schedule 24.01.2009
comment
«рефакторинг», разве это не перемещение / переименование кода? Вероятно, вы хотите переписать свой код.   -  person falstro    schedule 24.01.2009
comment
Большинство проблем связано с модульным тестированием. Я обошел проблему, инициализировав свои синглтоны во время тестирования, но мои тесты начали становиться невыносимыми.   -  person e.James    schedule 24.01.2009
comment
Становится трудным тестировать отдельный класс, когда вам сначала нужно инициализировать половину вашего пользовательского интерфейса.   -  person e.James    schedule 24.01.2009
comment
Отличный вопрос, но настоящая причина, по которой я изменил это, заключалась в разглагольствовании Стива Йегге о синглтонах и его мимолетных ударах по C ++. Не могу пропустить, нужно прочитать.   -  person zanussi    schedule 24.01.2009
comment
@roe: рефакторинг не всегда должен быть связан с перемещением и переименованием кода, но также и с очисткой. в основном вы можете вырвать и заменить внутренности и по-прежнему получить желаемый результат, но с той разницей, что будут другие побочные эффекты.   -  person Spoike    schedule 24.01.2009


Ответы (8)


Внедрение зависимостей - ваш друг.

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

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

person matt b    schedule 23.01.2009

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

person Eddie Deyo    schedule 23.01.2009

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

person David Schmitt    schedule 23.01.2009
comment
Разве это не просто синглтон с другим именем? Вместо того, чтобы все компоненты в вашем приложении имели (необъявленные) зависимости от синглтонов, теперь они зависят от вашего ApplicationContext. Звучит как глобальное состояние с другим названием. - person matt b; 24.01.2009
comment
Ну синглтоны являются глобальным состоянием. Проблемы возникают из-за того, что static является глобальным для виртуальной машины, а не для приложения, и поэтому его нельзя легко заменить / смоделировать / контролировать. - person David Schmitt; 24.01.2009
comment
Теперь вы заменили антипаттерн Синглтон на антишаблон «Объект Бога». Удачи с этим - person 1800 INFORMATION; 24.01.2009
comment
@ 1800-INFORMATION: я отредактировал свой ответ, чтобы лучше отразить то, что я делаю на самом деле. В конце концов, я думаю, что преимущества возможности заменить / унаследовать мой ApplicationContext перевешивают недостатки объекта God. - person David Schmitt; 26.01.2009

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

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

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

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

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

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

person Tac-Tics    schedule 05.05.2009
comment
+1 за ваш комментарий о синглтонах и глобальных объектах, которые имеют свое место. С тех пор я решил по-прежнему использовать их в нескольких местах, где они имеют смысл. Не в магазине, но определенно в некоторых классах пользовательского интерфейса верхнего уровня. - person e.James; 05.05.2009
comment
При этом я не согласен с вашей аналогией с поворотниками. Я приучил себя относиться ко всем операциям, связанным с вождением, как к рефлексам. Если машина собирается повернуть, я использую сигнал. Это дает моему мозгу возможность думать о важных решениях при вождении без необходимости подсчитывать количество автомобилей позади меня, прежде чем просто щелкнуть переключателем. - person e.James; 05.05.2009

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

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

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

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

person Spoike    schedule 23.01.2009

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

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

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

person Charlie Martin    schedule 23.01.2009
comment
Синглтоны, как правило, затрудняют тестирование кода, IME. Тот факт, что в течение жизненного цикла реального приложения будет только один экземпляр, не означает, что создание нового экземпляра для каждого теста нецелесообразно. - person Jon Skeet; 24.01.2009
comment
Синглтоны не обязательно зло, однако шаблоны синглтонов обычно таковы по целому ряду причин (например, возможность модульного тестирования) - person falstro; 24.01.2009
comment
Я думал о сценарии с двумя магазинами, но поскольку пользователю требуется войти в конкретный магазин, может быть активен только один магазин. - person e.James; 24.01.2009
comment
eJames, что, если кому-то в какой-то момент понадобится войти в два? Дело в том, чтобы подумать о степенях свободы в спецификации; это очевидное место, которое может измениться. - person Charlie Martin; 24.01.2009
comment
@ Чарли: Достаточно честно. В любом случае, я решил избавиться от синглтонов. - person e.James; 25.01.2009

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

person falstro    schedule 23.01.2009

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

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

person 1800 INFORMATION    schedule 23.01.2009