Здравствуйте, Пётр Седов, Вы писали:
ПС>Глобальная переменная и глобальный объект – это очень разные вещи. В отличие от переменной, работа с объектом идёт через интерфейс. Объект может проверять параметры методов и как-то реагировать на неправильные значения (assert, исключение, …).
Если у синглетона есть состояние и определен интерфейс для изменения этого состояние, то все эти различия — несущественны с точки зрения возникающей проблемы. И ассертами с исключениями ты тут ровным ничего не сделаешь.
ПС>Всё-таки, чем опасен singleton?
Ну давай по порядку.
1. Есть такое отличное правило SRP (Single Responsibility Principle). Синглетон его нарушает, т.к. кроме своих бизнес задач(например, доступ к конфигу) он еще решает и задачу контролирования количества своих экземпляров. Фабрика, например, от такого недостатка избавлена.
2. Синглетон повышает связность. Недостатки высокой связности я думаю приводить не надо.
3. Наличие синглетонов понижает тестируемость(testability). Отчасти это следствие второго пункта. Но есть и еще одна причина. Если синглетон предоставляет интерфейс для изменения своего состояния, то тесты теряют независимость друг от друга, т.к. результат работы одного теста может повлиять на результат другого.
4. Еще приводят аргумент, что синглетон противоречит LSP. Для меня этот довод несколько спорный, поэтому я пишу этот пункт только по причине частого его упоминания.
Теперь к моему посту. Почему я считаю, что синглетон нужно приводить в конце книги. Бесспорно, синглетон — это инструмент. Как и любой другой инструмент, он может быть использован по месту(хотя для меня это всегда некоторый smell). Главный его недостаток, что он потворствует плохому дизайну. И действительно, зачем строить компонентую модель, разбираться с сервис провайдерами, читать про какую-то dependency injection, когда можно на раз-два свалять синглетон, да еще и с double checking locking в GetInstance. Практика показывает, что если немного подумать, то в большинстве случаев можно избежать использование синглетонов. Да, в краткосрочной перспективе — вы потеряете время "на подумать", но в долгосрочной — выиграете. Увы, понимают это далеко не все. Потому что синглетон — это паттерн, а паттерны — это круто.
Петр, у тебя какая-то странная логика. Твой вопрос был какой?
Всё-таки, чем опасен singleton?
Еще раз и по порядку.
1. SRP. От того, что в Scala есть "определения объектов", а C++ — подумать только! — статические члены, синглетон перестает нарушать SRP?
2. С тем, что синглетон увеличивает связность, я вижу ты согласился. Этим он и опасен. Точка! Если тебе хочется поговорить на тему "Сильная связность — это хорошо" так и скажи.
3. Testability. Опять не понял. От того, что есть классы, которые плохо поддаются тестированию следует, что синглетон не влияет отрицательно на тестируемость? Или может влияние чудесным образом пропадает от того, что юнит-тесты не всегда эффективны? Или понижение тестируемости приложения — это хорошо? Кстати, речь идет не тестируемости синглетона, с этим как правило проблем нет, а приложении в целом, и классов, которые его используют, в особенности.
4. По поводу нагнетатия сложности. Ты не поверишь, но все эти компонентные модели и сервис провайдеры как раз и придуманы для борьбы со сложностью.
5. Пример с выводом отладочной информации — крайне неудачен. А что если тебе понадобиться поменять логику вывода? А если она должна быть разной для разных классов? А если тебе надо поменять форматирование? И к тому же, протаптывание интерфейса логгера во всех методах — не единственная альтернатива.
6. По поводу double checking locking — это ирония была.
Ну и еще раз о глобальных объектах и глобальных переменных. Их основная проблема — именно в глобальности и способности менять свое состояние. Вычеркни одно из этих условий — и все станет значительно проще. Локальные переменные большой проблемы не представляют и глобальные константы — тоже.
Здравствуйте, Graf Alex, Вы писали:
GA>Пару вопросов по паттернам...
GA>вопрос 1: отвлеченный от дела... чисто для научного интереса... GA>Слышал, что Singletone уже некоторые считают антипаттерном. Мол что это такое: объект доступен из всей программы? А что сейчас вместо синглтона модно использовать?
Я сейчас крамольную мысль скажу. С моей точки зрения, основной недостаток синглетона в том, что он оказался первым паттерном в знаменитой книжке и стал эдаким введением в оные. Как результат: юное дарование услышав, что лишь сакральное знание о паттернах способно за 24 часа из junior-а сделать senior-а, бежит в магазин покупать очередное "Введение в паттерны", чтобы прочитать первые 5-10 страниц, т.к. на большее его не хватает. Не пройдет и дня, как дарование уже внедряет прочитанное в жизнь, паралелльно добавляя в свое резюме "Богатый опыт применения паттернов" и приписывая к "Зарплатным ожиданиям" еще 500 долларов. И все бы ничего, да вот на первых 5-10 страницах любого "Введения в" у нас как раз и располагается описание синглетона. Простого и понятного, как глобальная переменная, а потому и настолько же опасного. Ведь фабрикой, оберткой и прочими мостами проект попортить значительно труднее, да и придумать, куда их впихнуть тоже нелегко.
В действительности же описание синглетона надо было поместить в середину или даже в самый конец книжки, чтобы до него добрались лишь старательные читатели, среди которых процент "шапкозакидателей" стремиться к нулю.
GA>Вопрос 2: GA>Хотелось бы предоставить пользователю моей библиотеки иметь возможность заменять мои модули своими, но при этом если пользователь ничего не заменял используются реализации из моей библиотеки.
Как тебе совершенно правильно сказали, у тебя получаются обыкновенные плагины. То что написал, можно перефразировать "Мне нужно контролировать создание инстанса модуля, по следующему правилу. Если есть пользовательская реализация модуля — использовать ее, в противном случае — использовать реализацию по умолчанию". Чтобы эффективно контролировать создание инстанса модуля, нужно, чтобы любая попытка создания инстанса модуля проходила через это правило. В самом простом варианте — это обыкновенный фабричный метод.
: ПС>Функция GetNumFromEdit использует глобальный C/C++ heap. Функция GetNumFromEdit2 принимает указатель на объект, реализующий интерфейс MemManager, поэтому ей можно подсунуть любой распределитель памяти (например, thread-local heap).
Понятно. Вместо синглтона подсовываем параметр. Но ведь, чтобы вместо синглтона передать параметр, надо сначала узнать этот параметр Откуда его получит вызывающая функция? Например, как быть с соединением с БД? Пусть библиотека имеет несколько точек входа. Ни в одной нельзя быть уверенным, что она будет вызвана первой. Но как только потребуется соединение с БД, надо либо подключиться (если нет подключения), либо взять существующее подключение.
Если возложить обязанность создать и передать подключение на пользователя библиотеки, то пользователь должен будет знать о внутренних деталях функционирования библиотеки. Реально же ему эти подробности могут быть вообще не интересны: БД там используется или файлы, например.
Реализовать подключение в каждой точке входа? А если могут использоваться несколько точек входа одновременно? Например, сначала выводится блок новостей, а потом блок анонсов.
Здравствуйте, Ужасть бухгалтера, Вы писали:
УБ>Извиняюсь, что вмешиваюсь...
Это форум, а не личная переписка .
УБ>А можно привести пример замены синглтона на альтернативное решение?
Пример в этой же ветке здесь
Функция GetNumFromEdit использует глобальный C/C++ heap. Функция GetNumFromEdit2 принимает указатель на объект, реализующий интерфейс MemManager, поэтому ей можно подсунуть любой распределитель памяти (например, thread-local heap).
Здравствуйте, Graf Alex, Вы писали:
GA>Обычно я пользуюсь синглтоном как единственным объектом... Мне в общем то пофиг что он доступен везде... GA>Просто хотел услышать мнение окружающих...
Модуль должен на вход получать внешние объекты (через ссылки/указатели/интерфейсы — не суть). О том, что провайдером данного объекта является синглтон, модулю знать не надо, иначе будут траблы при юнит-тестировании или рефакторинге. А так — подсунул модулю на вход какой-то другой инстанс (не от синглтона, а тот же мок-объект, например) — и все. Никаких редактирований кода, зависимостей и т.п.
Здравствуйте, Graf Alex, Вы писали: GA>Слышал, что Singletone уже некоторые считают антипаттерном. Мол что это такое: объект доступен из всей программы? А что сейчас вместо синглтона модно использовать?
Пуристы, наверное. Они любят догмы.
На самом деле singleton – вполне нормальное решение. Несколько примеров:
* Windows-реестр. Это глобальный объект в рамках компьютера. Доступен всем. Используется, к примеру, для регистрации COM-компонентов.
* Heap (распределитель памяти). Это глобальный объект в рамках процесса. Доступен всем (malloc/free, new/delete). Кстати, помимо C/C++ heap-а, имеется глобальный process heap от Windows (WinAPI-шная функция GetProcessHeap возвращает handle этого heap-а).
* Очередь оконных сообщений. Это глобальный объект в рамках потока (thread). Доступна всем. Кстати, вначале поток не имеет очереди. Windows создаёт очередь неявно, когда поток первый раз вызывает user-функцию (то есть WinAPI-шную функцию из user32.dll).
Здравствуйте, MaximVK, Вы писали:
ПС>>Глобальная переменная и глобальный объект – это очень разные вещи. В отличие от переменной, работа с объектом идёт через интерфейс. Объект может проверять параметры методов и как-то реагировать на неправильные значения (assert, исключение, …). MVK>Если у синглетона есть состояние и определен интерфейс для изменения этого состояние, то все эти различия – несущественны с точки зрения возникающей проблемы.
Различие между глобальным объектом и «эквивалентной» кучкой глобальных переменных очень существенно. Например, есть глобальный объект:
Теперь нужно добавить многопоточность. Вариант с глобальным объектом переносит это более/менее безболезненно (так как интерфейс отделён от реализации):
// Java
class PrintOptions
{
public static void main(...)
{
...
}
}
// Scala
object PrintOptions
{
def main(...) : unit =
{
...
}
}
…
* В Scala кроме определений классов есть определения объектов (начинающиеся с object). Определения объектов определяют класс с одним экземпляром – который иногда называют singleton-объектом. В приведенном примере singleton-объект PrintOptions содержит функцию-член main. Singleton-объекты в Scala заменяют статические части Java-классов.
Да и в C++ не обязательно извращаться с GetInstance и private-изацией конструктора. Вполне сойдёт класс, в котором все члены static, например:
) усложняет код.
* Снижается эффективность программы (из-за косвенности).
MVK>3. Наличие синглетонов понижает тестируемость(testability). Отчасти это следствие второго пункта. Но есть и еще одна причина. Если синглетон предоставляет интерфейс для изменения своего состояния, то тесты теряют независимость друг от друга, т.к. результат работы одного теста может повлиять на результат другого.
Во-первых, не всё можно тестировать (например, как тестировать Clock?).
Во-вторых, если нужно тестировать сам глобальный объект и он реализован без GetInstance (который на самом деле не нужен), то можно закрыть глаза на требование единственности экземпляра и тестировать независимых близнецов singleton-а, например:
class Item
{
// неявное копирование запрещено
Item(const Item& Source); // нет реализации
Item& operator=(const Item& Source); // нет реализацииpublic:
void Cache_AddRefs();
void Cache_Release();
...
// реализацияprivate:
int m_CacheRefs;
...
};
Item g_Item; // singletonvoid TestItem()
{
// тест 1
{
Item it; // независимый близнец singleton-а
...
}
// тест 2
{
Item it;
...
}
...
}
.
MVK>4. Еще приводят аргумент, что синглетон противоречит LSP. Для меня этот довод несколько спорный, поэтому я пишу этот пункт только по причине частого его упоминания.
Liskov's substitution principle гласит (здесь):
All classes derived from a base class should be interchangeable when used as a base class.
По-моему, более/менее удачные singleton-ы не нарушают LSP. Например, в стандартной библиотеке C++ есть глобальный объект std::cout, наследующий от класса std::ostream (строго говоря, это не класс, а typedef). Есть функция WriteExp (здесь
Эта функция пишет выражение в поток. Я вполне могу подсунуть этой функции std::cout:
Exp* pFormula = ...;
WriteExp(&cout, pFormula);
MVK>Бесспорно, синглетон — это инструмент. … Главный его недостаток, что он потворствует плохому дизайну.
Иногда отказ от singleton-а приводит к тому, что из интерфейса торчат уши реализации. Вот это действительно плохой дизайн. Например, есть функция CalcSomething:
double CalcSomething(параметры вычисления)
{
double Res = 0;
for (...)
{
...
Res += ...;
}
return Res;
}
Если эта функция выдаёт ошибочный результат, то можно сделать отладку в стиле printf:
#include <iostream> // => в любой точке кода доступен std::coutdouble CalcSomething(параметры вычисления)
{
double Res = 0;
for (...)
{
...
Res += ...;
std::cout << "Res = " << Res << std::endl;
}
return Res;
}
Это возможно, так как std::cout – singleton, он доступен глобально. Если отказаться от singleton-а, то придётся сделать как-то так:
#include <iostream>
double CalcSomething(параметры вычисления, std::ostream* pDebugStream)
{
double Res = 0;
for (...)
{
...
Res += ...;
*pDebugStream << "Res = " << Res << std::endl;
}
return Res;
}
(Вместо std::ostream можно использовать абстрактный интерфейс-класс.) Но это хуже, чем жёстко использовать std::cout. Во-первых, деталь реализации (отладочная печать) проникает в интерфейс (появился дополнительный параметр). То есть из интерфейса торчат уши реализации. Во-вторых, нужно пройтись по всем местам, где вызывается CalcSomething, и снабдить вызов каким-то потоком (скорее всего, тем же std::cout-ом). Кстати, это относится не только к std::cout, но и к аналогичным вещам (printf, System.Console, OutputDebugString, ATLTRACE, …).
MVK>И действительно, зачем строить компонентую модель, разбираться с сервис провайдерами, читать про какую-то dependency injection,
Да, зачем нагнетать сложность? По-моему, здесь
Мой жизненный опыт говорит о том, что сложности придумывают в двух случаях:
1) Не умеют работать
2) Раздувают объём работ
Ну и как дополнительный вариант — комбинация этих двух.
MVK>когда можно на раз-два свалять синглетон,
По-моему, начинать надо с простых решений, а усложнять только по необходимости.
MVK>да еще и с double checking locking в GetInstance.
Зачем такие сложности? Я привёл пример singleton-а (Clock), там всё гораздо проще.
MVK>Практика показывает, что если немного подумать, то в большинстве случаев можно избежать использование синглетонов. Да, в краткосрочной перспективе — вы потеряете время "на подумать", но в долгосрочной — выиграете.
Нет такого выбора, что либо «не думая по-быстрому сделать singleton», либо «подумать и сделать не singleton». Есть выбор либо «подумать и сделать singleton», либо «подумать и сделать не singleton». То есть подумать надо обязательно.
MVK>Увы, понимают это далеко не все.
Не все согласны с утверждением «singleton – зло».
) усложняет код.
Однако MemManager это уже довольно крайний случай, вряд ли кому нужен менджер памяти отличный от CRT.
ПС>* Снижается эффективность программы (из-за косвенности).
ИМО, это никогда не будет узким местом, поэтому экономить пару тактов проца бессмысленно.
ПС>Во-первых, не всё можно тестировать (например, как тестировать Clock?).
Зато очень хочется тестировать код который исползует этот Clock, причем хочется контролировать значения которые этот Clock возвращает.
ПС>Во-вторых, если нужно тестировать сам глобальный объект и он реализован без GetInstance (который на самом деле не нужен), то можно закрыть глаза на требование единственности экземпляра и тестировать независимых близнецов singleton-а
Обычно проблема не с тестом синглетона, а с тестами кода его использующего.
"For every complex problem, there is a solution that is simple, neat,
and wrong."
вопрос 1: отвлеченный от дела... чисто для научного интереса...
Слышал, что Singletone уже некоторые считают антипаттерном. Мол что это такое: объект доступен из всей программы? А что сейчас вместо синглтона модно использовать?
Вопрос 2:
Пишу библиотеку. В ней есть ModuleA, ModuleB, ModuleC. Для каждого модуля объявлен интерфейс, который в принципе укладывается в одну функцию типа SomeResultType runModuleA(<parameters>); Хотя как интерфейс с одним виртуальным методом тоже возможен.
Реализации модулей понятно друг о друге ничего не знают и общаются только через интерфейсы.
Хотелось бы предоставить пользователю моей библиотеки иметь возможность заменять мои модули своими, но при этом если пользователь ничего не заменял используются реализации из моей библиотеки.
Проще говоря пускай ModuleA и ModuleB вызывают ModuleC в своей реализации.
Хотелось код вызова писать так:
Т.е. по сути пользователь должен как то регистрировать свой модуль.
Какие есть подходящие решения? Можно было бы конечно воспользоваться дефолтными параметрами, но как то некультурно получается. Да и параметров может быть наверное тоже много — какие то дефолтные, какие то нет...
Здравствуйте, Graf Alex, Вы писали:
GA>Пару вопросов по паттернам...
GA>вопрос 1: отвлеченный от дела... чисто для научного интереса... GA>Слышал, что Singletone уже некоторые считают антипаттерном. Мол что это такое: объект доступен из всей программы? А что сейчас вместо синглтона модно использовать?
А при чем тут синглетон и доступность всей программе? Синглетон — это возможность создавать ограниченное (обычно 1) количество объектов одного класса. Тебе что нужно — чтобы все классы видели твой объект или чтобы экземпляр был 1?
GA>Вопрос 2: GA>Пишу библиотеку. В ней есть ModuleA, ModuleB, ModuleC. Для каждого модуля объявлен интерфейс, который в принципе укладывается в одну функцию типа SomeResultType runModuleA(<parameters>); Хотя как интерфейс с одним виртуальным методом тоже возможен. GA>Реализации модулей понятно друг о друге ничего не знают и общаются только через интерфейсы.
GA>Хотелось бы предоставить пользователю моей библиотеки иметь возможность заменять мои модули своими, но при этом если пользователь ничего не заменял используются реализации из моей библиотеки.
GA>Проще говоря пускай ModuleA и ModuleB вызывают ModuleC в своей реализации. GA>Хотелось код вызова писать так: GA>
GA>Т.е. по сути пользователь должен как то регистрировать свой модуль.
А как твоя софтина без регистрации узнает, что есть еще какие-то модули? Посмотри на тот же СОМ — приходжится регистрится, чтобы твои интерфейсы и реализации были доступны.
GA>Какие есть подходящие решения? Можно было бы конечно воспользоваться дефолтными параметрами, но как то некультурно получается. Да и параметров может быть наверное тоже много — какие то дефолтные, какие то нет...
Трудно тебе что-либо посоветовать, потому как задача ну очень вобщем поставлена.
З.Ы. Вообще для того, чтобы можно было вот так вот прозрачно подменять реализации без регистрации придется укладывать твои модули в некоторое Прокрустово ложэ — например сказать, что есть папка, в эту папку пользователь должен положить свою длл-ну, при инициализации софтина сканит папку, грузит длл-ну и получает указатель на реализацию пользователя. Ну и соотвественно интерфейс этих длл-ин должен быть унифицирован. Короче говоря, тот же СОМ, только руками.
Здравствуйте, Glоbus, Вы писали:
G>Здравствуйте, Graf Alex, Вы писали:
GA>>Пару вопросов по паттернам...
GA>>вопрос 1: отвлеченный от дела... чисто для научного интереса... GA>>Слышал, что Singletone уже некоторые считают антипаттерном. Мол что это такое: объект доступен из всей программы? А что сейчас вместо синглтона модно использовать? G>А при чем тут синглетон и доступность всей программе? Синглетон — это возможность создавать ограниченное (обычно 1) количество объектов одного класса. Тебе что нужно — чтобы все классы видели твой объект или чтобы экземпляр был 1?
Вот и меня это же смущает...
Обычно я пользуюсь синглтоном как единственным объектом... Мне в общем то пофиг что он доступен везде...
Просто хотел услышать мнение окружающих...
GA>>Вопрос 2: GA>>Пишу библиотеку. В ней есть ModuleA, ModuleB, ModuleC. Для каждого модуля объявлен интерфейс, который в принципе укладывается в одну функцию типа SomeResultType runModuleA(<parameters>); Хотя как интерфейс с одним виртуальным методом тоже возможен. GA>>Реализации модулей понятно друг о друге ничего не знают и общаются только через интерфейсы.
GA>>Хотелось бы предоставить пользователю моей библиотеки иметь возможность заменять мои модули своими, но при этом если пользователь ничего не заменял используются реализации из моей библиотеки.
G>А как твоя софтина без регистрации узнает, что есть еще какие-то модули? Посмотри на тот же СОМ — приходжится регистрится, чтобы твои интерфейсы и реализации были доступны.
Нет, понятное дело, что модули как то надо регистрировать... GA>>Какие есть подходящие решения? Можно было бы конечно воспользоваться дефолтными параметрами, но как то некультурно получается. Да и параметров может быть наверное тоже много — какие то дефолтные, какие то нет... G>Трудно тебе что-либо посоветовать, потому как задача ну очень вобщем поставлена.
Короче говоря, какие есть механизмы, которые позволят юзеру потставлять вместо дефолтных модулей свои?
Здравствуйте, Graf Alex, Вы писали:
GA>Пару вопросов по паттернам...
спроси гугл на тему "IOC контейнеры" — как раз то что описано, даже еще более обобщенно. Но в С++ я такого не видел.
Здравствуйте, dotidot, Вы писали:
D>спроси гугл на тему "IOC контейнеры" — как раз то что описано, даже еще более обобщенно. Но в С++ я такого не видел.
ну или более приземлено: "plugin architecture"
Re[3]: подскажите паттерн №2
От:
Аноним
Дата:
15.06.07 07:09
Оценка:
GA>Короче говоря, какие есть механизмы, которые позволят юзеру потставлять вместо дефолтных модулей свои?
Ну первое что приходит на ум — реализуйте у себя COM
Здравствуйте, dotidot, Вы писали:
D>Здравствуйте, dotidot, Вы писали:
D>>спроси гугл на тему "IOC контейнеры" — как раз то что описано, даже еще более обобщенно. Но в С++ я такого не видел. D>ну или более приземлено: "plugin architecture"
IoC это автоматичиское разруливание зависимостей между доменными объектами, а то что автору нужно скорее плагинная архитектура.
Второй вопрос распадается на два: Как сделать архитектурно?
Как вариант, можно сделать конфигурационный файл, в котором записано какие модули следует загружать и из какого файла. То есть,к примеру, в конфигурационном фале пишутся так называемые "точки входа". То есть места в библиотеке, куда может быть загружен внешний модуль, реализующий определённый интерфейс. В дефолтовом конфигурационном фале прописываются твои модули, юзер может вместо них зарегистрировать свои.
Как реализовать технически?
Можно сделать через COM, то есть в точке входа прописывается guid или progId ком объекта, реализующего заданный интерфейс. Можно обойтись без кома — в каждой дллке будет функция ICustomMidule* Load(), возвращающая интерфейсный указатель. Если дллки динамически линкуются с CRT, то можно смело при необходимости использовать такой код:
Соответсвенно, всяки shared_ptr<> вполне актуальны. Ограничение — плагины и библиототека должны быть скомпилированы одним и тем же компилятором. При статической линковке надо кроме Load делать ещё и Free в дллке, то есть что-то типа
Free(someModule);
Ещё как вариант — возвращать не интефрейс, а handle объекта из дллки и, соответсвенно, использовать голый С. Это позволит получить более переносимое решение. Пример кода:
Здравствуйте, Graf Alex, Вы писали:
GA>Какие есть подходящие решения? Можно было бы конечно воспользоваться дефолтными параметрами, но как то некультурно получается. Да и параметров может быть наверное тоже много — какие то дефолтные, какие то нет...
Здравствуйте, Пётр Седов, Вы писали:
ПС>* Heap (распределитель памяти). Это глобальный объект в рамках процесса. Доступен всем (malloc/free, new/delete).
Я ошибся.
В рамках процесса может быть несколько C/C++ heap-ов:
Здесь App.exe использует статически link-уемый C/C++ run-time. При этом App.exe динамически link-уется с Some.dll, который в свою очередь динамически link-уется с msvcrt.dll. В результате в рамках процесса одновременно живут два C/C++ heap-а (зелёные прямоугольники).
ПС>Кстати, помимо C/C++ heap-а, имеется глобальный process heap от Windows (WinAPI-шная функция GetProcessHeap возвращает handle этого heap-а).
Тут всё правильно – Windows process heap действительно один на весь процесс (видимо, поэтому он и называется «process heap» ).
Здравствуйте, Пётр Седов, Вы писали:
ПС>Пуристы, наверное. Они любят догмы.
Скорее практики
ПС>На самом деле singleton – вполне нормальное решение. Несколько примеров: ПС>* Windows-реестр. Это глобальный объект в рамках компьютера. Доступен всем. Используется, к примеру, для регистрации COM-компонентов.
Опять же все зависит от точки зрения. Действительно можно сказать, что реестр он глобальный в рамках компьютера и напрямую вызывать API. Но можно скзать что это конфигурация и где она хранится не имеет значения. Что мешает иметь несколько конфигураций одновременно?
Простой пример из жизни. Есть софтина которая позволяет работать с несколькими торговыми системами одновременно. Т.е. одновременно может быть активно несколько конфигураций.
Реализовывать такую софтины с помощью синглетона, ИМО, весьма не удобно.
ПС>* Heap (распределитель памяти). Это глобальный объект в рамках процесса. Доступен всем (malloc/free, new/delete). Кстати, помимо C/C++ heap-а, имеется глобальный process heap от Windows (WinAPI-шная функция GetProcessHeap возвращает handle этого heap-а).
Хипов может быть несколько. А кроме того этого довольно часто это становится узким местом в многопоточных приложениях (особенно если кто-то, как в STLPort, додумается использовать Sleep для реализации мьютексов). И хотелось бы иметь возможность иметь Хип на поток.
"For every complex problem, there is a solution that is simple, neat,
and wrong."
Здравствуйте, AndrewJD, Вы писали:
ПС>>Пуристы, наверное. Они любят догмы. AJD>Скорее практики
Догмы и категоричные утверждения свойственны именно теоретикам. Практики в каждом конкретном случае смотрят, подходит ли приём хорошо для решения задачи.
Классическая догма – «goto considered harmful». Но, к примеру, Фредерик Брукс в книге «Мифический человеко-месяц»
пишет: «… некоторые догматически избегают всех GO TO, что представляется чрезмерным.» (Глава 13. Целое и части > Проектирование без ошибок > Структурное программирование).
ПС>>На самом деле singleton – вполне нормальное решение. Несколько примеров: ПС>>* Windows-реестр. Это глобальный объект в рамках компьютера. Доступен всем. Используется, к примеру, для регистрации COM-компонентов. AJD>Опять же все зависит от точки зрения.
Вне зависимости от точки зрения, реестр – глобальный объект в рамках компьютера (точнее, в рамках экземпляра Windows; на компьютере может быть установлено несколько Windows, и каждый будет иметь свой отдельный реестр).
AJD>Действительно можно сказать, что реестр он глобальный в рамках компьютера
Только так и можно сказать (в том смысле, что все процессы, работающие под управлением Windows, обращаются к одному и тому же реестру; изменения, внесённые одним процессом, становятся видны остальным процессам).
AJD>и напрямую вызывать API. Но можно скзать что это конфигурация и где она хранится не имеет значения. Что мешает иметь несколько конфигураций одновременно?
Мы обсуждаем реестр или config? Если config, то действительно полезно изолировать реализацию, чтобы наружу из config-а не торчали уши реестра. Тогда эту реализацию можно безболезненно заменить и хранить данные config-а не в реестре, а, скажем, в текстовом файле (XML, …).
AJD>Простой пример из жизни. Есть софтина которая позволяет работать с несколькими торговыми системами одновременно. Т.е. одновременно может быть активно несколько конфигураций. AJD>Реализовывать такую софтины с помощью синглетона, ИМО, весьма не удобно.
Никто и не утверждает, что singleton хорошо подходит во всех случаях. Если нужно несколько конфигураций одновременно, то понятно, что config вряд ли стоит делать singleton-ом.
Просто Graf Alex в корневом сообщении
задал примерно такой вопрос: «Singleton – это зло?». Я ответил, что не зло, и привёл примеры более/менее удачных (по-моему) singleton-ов. То есть показал, что в некоторых (но не всех) случаях singleton хорошо подходит.
ПС>>* Heap (распределитель памяти). Это глобальный объект в рамках процесса. Доступен всем (malloc/free, new/delete). Кстати, помимо C/C++ heap-а, имеется глобальный process heap от Windows (WinAPI-шная функция GetProcessHeap возвращает handle этого heap-а). AJD>Хипов может быть несколько.
Опять же, никто не утверждает, что в процессе должен быть ровно один heap. Просто удобно, чтобы в любой точке кода был доступен распределитель памяти (у которого, вероятно, pool-ы уже прогреты и выделение блока памяти сводится к просто доставанию оного из подходящего pool-а). Но иногда действительно полезно параметризовать распределение памяти. Например, STL-контейнеры позволяют это (хотя и криво
).
Кстати, распределитель виртуальной памяти (доступный через WinAPI-шные функции VirtualAlloc, VirtualFree) принципиально один на весь процесс.
AJD>А кроме того этого довольно часто это становится узким местом в многопоточных приложениях (особенно если кто-то, как в STLPort, додумается использовать Sleep для реализации мьютексов). И хотелось бы иметь возможность иметь Хип на поток.
То есть singleton в рамках потока (thread).
Здравствуйте, Пётр Седов, Вы писали:
ПС>Догмы и категоричные утверждения свойственны именно теоретикам.
Лично я пришел к этому выводу путем набивания шишек .
ПС>Практики в каждом конкретном случае смотрят, подходит ли приём хорошо для решения задачи.
Безусловно. Проблема в том, что когда продукт развивается, когда-то простые, хорошо подходящие приемы превращаются в геморрой при сопровождении. Разумется, это касается не только синглтонов.
Опять же из практики я пришел к выводу, что глобальные, неявные зависимости от реализации это очень и очень плохо. Использывать же синглетон как средство контролирования количества экземпляров без предоставления глобального доступа не имеет особого смысла.
ПС>Вне зависимости от точки зрения, реестр – глобальный объект в рамках компьютера (точнее, в рамках экземпляра Windows; на компьютере может быть установлено несколько Windows, и каждый будет иметь свой отдельный реестр).
Это с точки зрения пользователя. Например ОС может рассматривать реестр как набор из файлов (покрайней мере 2 файлов)
ПС>Мы обсуждаем реестр или config? Если config, то действительно полезно изолировать реализацию, чтобы наружу из config-а не торчали уши реестра. Тогда эту реализацию можно безболезненно заменить и хранить данные config-а не в реестре, а, скажем, в текстовом файле (XML, …).
Так про то и речь, что в большинстве случаев стоит рассматривать config, а не реестр. Если ты конечно не пишешь сотину для работы с реестром .
ПС>Никто и не утверждает, что singleton хорошо подходит во всех случаях. Если нужно несколько конфигураций одновременно, то понятно, что config вряд ли стоит делать singleton-ом.
Фишка в том что часто-густо, при проектировании на это забивают. Т.е. сначала софтина могла поддерживать только одну кофигурацию и использование синглетона выглядело элегантным решением. По мере развития софтины, бизнес логика потребовала несколько конфигураций, а тут, опа, все прибито гвоздями к одному глобальному конфигу.
ПС>Я ответил, что не зло, и привёл примеры более/менее удачных (по-моему) singleton-ов. То есть показал, что в некоторых (но не всех) случаях singleton хорошо подходит.
Я бы сказал, что нужно три раза подумать прежде чем использовать синглетон, возможно есть более удобное и расширяемое решение.
ПС>Кстати, распределитель виртуальной памяти (доступный через WinAPI-шные функции VirtualAlloc, VirtualFree) принципиально один на весь процесс.
С этим приходится жить. Хотя раньше, когда 32mb памяти было очень много, многие делали свой распределитель виртуальной памяти со свопом на диск.
AJD>>А кроме того этого довольно часто это становится узким местом в многопоточных приложениях (особенно если кто-то, как в STLPort, додумается использовать Sleep для реализации мьютексов). И хотелось бы иметь возможность иметь Хип на поток. ПС>То есть singleton в рамках потока (thread).
Да. Но в рамках потока это уже значительно лучше чем один на процесс
"For every complex problem, there is a solution that is simple, neat,
and wrong."
Здравствуйте, AndrewJD, Вы писали:
AJD>Опять же из практики я пришел к выводу, что глобальные, неявные зависимости от реализации это очень и очень плохо.
Например, есть такой код:
int GetNumFromEdit(HWND hEdit)
{
assert(IsWindow(hEdit));
int Len = GetWindowTextLength(hEdit);
char* pBuf = newchar[Len + 1];
GetWindowText(hEdit, pBuf, Len + 1);
int Num = atoi(pBuf); // parsing ламерский, но сейчас речь не о нёмdelete[] pBuf;
return Num;
}
Этот код использует C/C++ heap (new/delete). C/C++ heap – это singleton в рамках процесса (считая, что App.exe и все DLL-и используют один и тот же C/C++ run-time, например msvcrt.dll). Можно отказаться от singleton-а и параметризовать распределение памяти:
class MemManager
{
public:
virtual void* AllocBlock(int Size) = 0;
virtual void FreeBlock(void* pMem, int Size) = 0;
};
int GetNumFromEdit2(HWND hEdit, MemManager* pMemManager)
{
assert(IsWindow(hEdit));
int Len = GetWindowTextLength(hEdit);
char* pBuf = static_cast<char*>(pMemManager->AllocBlock((Len + 1) * sizeof(char)));
GetWindowText(hEdit, pBuf, Len + 1);
int Num = atoi(pBuf);
pMemManager->FreeBlock(pBuf, (Len + 1) * sizeof(char));
return Num;
}
Но тогда деталь реализации (функция создаёт временный буфер) проникает в интерфейс. То есть было одно «зло» (использование глобального C/C++ heap-а), стало другое «зло» (из интерфейса торчат уши реализации). Поэтому отказ от singleton-а – не обязательно хорошо.
Ещё пример. Стандартная библиотека C/C++ предоставляет генератор случайных чисел (доступный через функции srand, rand). Это singleton в рамках потока (считая, что App.exe и все DLL-и используют один и тот же C/C++ run-time). С точки зрения теории это ужас, но на практике вполне годится для простых применений (которых, по-моему, большинство).
ПС>>Вне зависимости от точки зрения, реестр – глобальный объект в рамках компьютера (точнее, в рамках экземпляра Windows; на компьютере может быть установлено несколько Windows, и каждый будет иметь свой отдельный реестр). AJD>Это с точки зрения пользователя.
И с точки зрения программ, использующих реестр, тоже.
AJD>Например ОС может рассматривать реестр как набор из файлов (покрайней мере 2 файлов)
Реализация реестра (как он там на диске хранится) – дело вторичное. Главное, что реестр – глобальный объект в единственном экземпляре (в рамках экземпляра Windows).
ПС>>Мы обсуждаем реестр или config? Если config, то действительно полезно изолировать реализацию, чтобы наружу из config-а не торчали уши реестра. Тогда эту реализацию можно безболезненно заменить и хранить данные config-а не в реестре, а, скажем, в текстовом файле (XML, …). AJD>Так про то и речь, что в большинстве случаев стоит рассматривать config, а не реестр.
Я здесь
привёл реестр не как удачное место для хранения данных config-а, а как пример более/менее удачного singleton-а. Ведь иногда нужно именно глобальное хранилище. Например, для регистрации COM-компонентов. Ещё пример singleton-а в рамках Windows: running object table (ROT) в COM. RegisterActiveObject:
The RegisterActiveObject function registers the object to which punk points as the active object for the class denoted by rclsid. Registration causes the object to be listed in the running object table (ROT) of OLE, a globally accessible lookup table that keeps track of objects that are currently running on the computer.
ПС>>Никто и не утверждает, что singleton хорошо подходит во всех случаях. Если нужно несколько конфигураций одновременно, то понятно, что config вряд ли стоит делать singleton-ом. AJD>Фишка в том что часто-густо, при проектировании на это забивают.
Часто забивают на проектирование и пишут в плену framework-а (MFC, VCL, WinForms).
ПС>>Я ответил, что не зло, и привёл примеры более/менее удачных (по-моему) singleton-ов. То есть показал, что в некоторых (но не всех) случаях singleton хорошо подходит. AJD>Я бы сказал, что нужно три раза подумать прежде чем использовать синглетон, возможно есть более удобное и расширяемое решение.
По-моему, singleton и есть удобное решение. Не нужно всюду таскать ссылку на объект, так как он доступен везде.
ПС>>Кстати, распределитель виртуальной памяти (доступный через WinAPI-шные функции VirtualAlloc, VirtualFree) принципиально один на весь процесс. AJD>С этим приходится жить.
Разве с этим тяжело жить? Раз модель памяти плоская (flat), то иначе вроде и не сделать .
AJD>Хотя раньше, когда 32mb памяти было очень много, многие делали свой распределитель виртуальной памяти со свопом на диск.
И что, в рамках выполняющейся программы (в мире DOS-32 не было слова «процесс» ) жило несколько распределителей виртуальной памяти одновременно? Сомневаюсь. Скорее, распределитель виртуальной памяти так же был singleton-ом.
Кстати, писать свой распределитель виртуальной памяти с подкачкой страниц – это круто . Я во времена DOS-32 просто использовал DJGPP, там была поддержка DPMI.
Здравствуйте, MaximVK, Вы писали:
MVK>на первых 5-10 страницах любого "Введения в" у нас как раз и располагается описание синглетона. Простого и понятного, как глобальная переменная,
Глобальная переменная и глобальный объект – это очень разные вещи. В отличие от переменной, работа с объектом идёт через интерфейс. Объект может проверять параметры методов и как-то реагировать на неправильные значения (assert, исключение, …).
MVK>а потому и настолько же опасного.
Всё-таки, чем опасен singleton?
Например, стандартная библиотека C++ предоставляет глобальный текстовый поток для вывода (по умолчанию на консоль) – std::cout. В чём опасность?
Например, в библиотеке VCL (Delphi и C++ Builder) есть глобальные объекты Application и Screen (unit forms.pas):
{ Global objects }var
Application: TApplication;
Screen: TScreen;
В чём опасность?
Например, в .NET-е есть глобальный объект System.Text.Encoding.UTF8. В чём опасность?
Здравствуйте, Пётр Седов, Вы писали:
ПС>Пуристы, наверное. Они любят догмы.
Не пуристы, а практики.
На моей памяти небыло еще ни одного синглетона который бы не создал проблем.
Из-за одних больше, из-за других меньше(ибо быстро убрали). Но проблемы были из-за всех.
ПС>На самом деле singleton – вполне нормальное решение. Несколько примеров:
Бу-га-га...
ПС>* Windows-реестр. Это глобальный объект в рамках компьютера. Доступен всем. Используется, к примеру, для регистрации COM-компонентов.
Мелкософты (в не официальных разговорах) сами признают тот факт что это была ошибка.
И я с ними согласен.
ПС>* Heap (распределитель памяти). Это глобальный объект в рамках процесса. Доступен всем (malloc/free, new/delete). Кстати, помимо C/C++ heap-а, имеется глобальный process heap от Windows (WinAPI-шная функция GetProcessHeap возвращает handle этого heap-а).
Хип это вобще интимное дело рантайма. В коде ничего про хип быть не должно. Совсем.
Есть функция создать объект (ака конструктор), а где он будет создан и когда убит проблемы исключительно рантайма.
ПС>* Очередь оконных сообщений. Это глобальный объект в рамках потока (thread). Доступна всем. Кстати, вначале поток не имеет очереди. Windows создаёт очередь неявно, когда поток первый раз вызывает user-функцию (то есть WinAPI-шную функцию из user32.dll).
Тоже плохое решение.
Есть множество случаев когда может быть выгодно много очередей сообщений для одного потока.
Посмотри например на сингулярити (исследовательсякая ОС от мелкософт)... как ты думаешь почему там нет очереди сообщений потока?
... << RSDN@Home 1.2.0 alpha rev. 673>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, AndrewJD, Вы писали:
AJD>Однако MemManager это уже довольно крайний случай, вряд ли кому нужен менджер памяти отличный от CRT.
Да, во многих случаях использование C/C++ heap-а (singleton в рамках процесса) годится. Многие программисты используют malloc/free, или глобальные new/delete, или STL-контейнеры с allocator-ом по умолчанию. При этом мало кого волнует, что нарушается SRP (?), повышается связность, понижается тестируемость (так как это скорее теоретические проблемы). Программы просто работают.
Но иногда стандартный heap не годится, нужно что-то побыстрее. Тогда разумно от него отказаться и использовать распределитель памяти, заточенный под конкретные нужды. Я таким постоянно пользуюсь. Кстати, это не heap, а арена, и она всё равно singleton .
ПС>>Во-первых, не всё можно тестировать (например, как тестировать Clock?). AJD>Зато очень хочется тестировать код который исползует этот Clock, причем хочется контролировать значения которые этот Clock возвращает.
Тогда этот код должен использовать Clock не напрямую, а косвенно (через прослойку). Тогда можно подсунуть этому коду mock-часы. Но unit test-ы не всегда возможны/нужны, в этих случаях можно обойтись кодом попроще (с использованием singleton-а).
Здравствуйте, Пётр Седов, Вы писали:
ПС>Тогда этот код должен использовать Clock не напрямую, а косвенно (через прослойку). Тогда можно подсунуть этому коду mock-часы.
Если есть прослойка, то зачем синглетон?
ПС>Но unit test-ы не всегда возможны/нужны, в этих случаях можно обойтись кодом попроще (с использованием singleton-а).
Отказ от синглетонов повышает возможность использования unit test. Разве это не достаточная причина?
"For every complex problem, there is a solution that is simple, neat,
and wrong."
Здравствуйте, WolfHound, Вы писали:
ПС>>Пуристы, наверное. Они любят догмы. WH>Не пуристы, а практики.
То есть singleton-ы используются исключительно теоретиками ?
WH>На моей памяти небыло еще ни одного синглетона который бы не создал проблем. WH>Из-за одних больше, из-за других меньше(ибо быстро убрали). Но проблемы были из-за всех.
Странно . Меня часто singleton-ы устраивают. Когда перестают устраивать, заменяю чем-нибудь.
ПС>>На самом деле singleton – вполне нормальное решение. Несколько примеров: WH>Бу-га-га...
Это серьёзный аргумент .
ПС>>* Windows-реестр. Это глобальный объект в рамках компьютера. Доступен всем. Используется, к примеру, для регистрации COM-компонентов. WH>Мелкософты (в не официальных разговорах) сами признают тот факт что это была ошибка.
Можно ссылки на эти «неофициальные разговоры»? Или хотя бы аргументацию. И что именно Microsoft «признаёт»:
* Реестр – неудачное решение.
* Регистрировать COM-компоненты в реестре – неудачное решение.
?
Для регистрации COM-компонентов, по-моему, нужно какое-то глобальное хранилище. Не реестр, так что-нибудь другое. Но всё равно singleton в рамках Windows.
ПС>>* Heap (распределитель памяти). Это глобальный объект в рамках процесса. WH>Хип это вобще интимное дело рантайма. В коде ничего про хип быть не должно. Совсем. WH>Есть функция создать объект (ака конструктор), а где он будет создан и когда убит проблемы исключительно рантайма.
И что, использовать стандартный heap – грех? Подходит – используйте (это удобно, так как он доступен в любой точке кода). Не подходит – не используйте.
Вон STL-контейнеры (включая std::string) по умолчанию используют стандартный heap. Это часто подходит для решения задачи.
ПС>>* Очередь оконных сообщений. Это глобальный объект в рамках потока (thread). WH>Тоже плохое решение.
Может, это и плохое решение, но оно работает на практике. Также стоит упомянуть asynchronous procedure call (APC) очередь (доступную через WinAPI-шную функцию QueueUserAPC). Это singleton в рамках потока. Доступна всем, у кого есть соответствующие права.
WH>Есть множество случаев когда может быть выгодно много очередей сообщений для одного потока.
Я не утверждал, что очередь оконных сообщений потока хорошо подходит для всех задач. Но для некоторых задач (пользовательский интерфейс) подходит.
WH>Посмотри например на сингулярити (исследовательсякая ОС от мелкософт)... как ты думаешь почему там нет очереди сообщений потока?
Не знаю. Безопасность?
Здравствуйте, AndrewJD, Вы писали:
ПС>>Тогда этот код должен использовать Clock не напрямую, а косвенно (через прослойку). Тогда можно подсунуть этому коду mock-часы. AJD>Если есть прослойка, то зачем синглетон?
Чтобы в любой точке кода можно было узнать настоящее (не mock) время.
ПС>>Но unit test-ы не всегда возможны/нужны, в этих случаях можно обойтись кодом попроще (с использованием singleton-а). AJD>Отказ от синглетонов повышает возможность использования unit test. Разве это не достаточная причина?
Например, std::string использует стандартный heap (то есть singleton). При этом вполне можно писать unit test-ы, проверяющие std::string. Например:
Здравствуйте, Пётр Седов, Вы писали:
ПС>Странно . Меня часто singleton-ы устраивают. Когда перестают устраивать, заменяю чем-нибудь.
Просто еще шишек не набил.
А вот когда синглетон придется выдерать из десятка метров исходников подвязаных на этот синглетон...
ПС>Для регистрации COM-компонентов, по-моему, нужно какое-то глобальное хранилище. Не реестр, так что-нибудь другое. Но всё равно singleton в рамках Windows.
А кто сказал что у винды хороший дизайн?
ПС>И что, использовать стандартный heap – грех? Подходит – используйте (это удобно, так как он доступен в любой точке кода). Не подходит – не используйте. ПС>Вон STL-контейнеры (включая std::string) по умолчанию используют стандартный heap. Это часто подходит для решения задачи.
Для тебя мир ограничен С++? Сочувствую.
Я считаю что менеджер памяти программиста заботить вобще не должен.
1)Ибо программисту есть чем заняться кроме как памятью рулить.
2)Единственный способ безопасно и оптимально рулить памятью это статический анализ кода. Для языков типа С++ это конечно работать не будет. Ибо для этого нужно точно знать что происходит. В случе с С++ это не возможно.
ПС>Может, это и плохое решение, но оно работает на практике.
Малоли что работает на практике... вон С++ тоже работает на практике... правда плохо... слишком много телодвижений на пустом месте...
ПС>Также стоит упомянуть asynchronous procedure call (APC) очередь (доступную через WinAPI-шную функцию QueueUserAPC). Это singleton в рамках потока. Доступна всем, у кого есть соответствующие права.
Костыль. Служит исключительно для объезда других кривостей дизайна.
Я не могу представить зачем при хоть скольнибудь продуманном дизайне это может понадобиться.
ПС>Я не утверждал, что очередь оконных сообщений потока хорошо подходит для всех задач. Но для некоторых задач (пользовательский интерфейс) подходит.
Очередь сообщений для ГУИ? Привязанная к потоку? Хорошее решение? Ну-ну.
ПС>Не знаю. Безопасность?
Нет.
Банальное отделение мух от котлет.
Потоки отдельно. Очереди сообщений отдельно.
Таким образом каждый занимается своим делом.
И я в одном потоке могу работать с любым колличеством очередей сообщений.
... << RSDN@Home 1.2.0 alpha rev. 673>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Здравствуйте, Пётр Седов, Вы писали:
AJD>>Если есть прослойка, то зачем синглетон? ПС>Чтобы в любой точке кода можно было узнать настоящее (не mock) время.
А зачем ей быть синглетоном, с глобальной точкой доступа? . Это простойка вполне себе може пользоваться глобальными функциями, но она не должна быть доступна из произвольного места.
AJD>>Отказ от синглетонов повышает возможность использования unit test. Разве это не достаточная причина? ПС>Например, std::string использует стандартный heap (то есть singleton). При этом вполне можно писать unit test-ы.
ИМО, heap это вырожденый случай. Давай возьмем какой-нибудь другой синглетон который реализует логику приложения, а не функционирования рантайма.
"For every complex problem, there is a solution that is simple, neat,
and wrong."
ПС>>Странно . Меня часто singleton-ы устраивают. Когда перестают устраивать, заменяю чем-нибудь. WH>Просто еще шишек не набил. WH>А вот когда синглетон придется выдерать из десятка метров исходников подвязаных на этот синглетон...
Извиняюсь, что вмешиваюсь... А можно привести пример замены синглтона на альтернативное решение? Просто интересно.