Comments 128
Разработчики на Spring:
- Синглтон зло? Ух ты!
Автор теоретик?
Вот уже 20 лет я преподаю программирование
Он самый.
Всё, что вы хотели сказать ему, вы можете сказать мне и моему другу «Смит-Вессону». clint_eastwood.jpg
Я практик, и подтверждаю, что зло. Может, не самый злой корень, но уж точно не добро.
Двадцать с лишним лет я преподаю программирование наблюдаю, как проектировщики говорят себе и окружающим: я точно, абсолютно, на миллион процентов уверен, что уж этот-то экземпляр своего типа будет только один.
А затем они идут и делают функцию, которая возвращает корневой интерфейс Direct3D. Интерфейсы COM, как вы знаете, надо запрашивать через ::CoCreateInstance()
в любых потребных количествах, но… На дворе 1996 год. И как, спрашивается, вы себе это представляете — целых ДЖВА 3D-ускорителя в одном компьютере?! Только пять самых богатых корпораций в мире могут позволить себе два ускорителя ценой в автомобиль, и все они уже купили себе рабочие станции от Silicon Graphics! Поэтому нет, держите вместо этого глобальную функцию.
Две кнопки “Start”? Конечно же, такое может присниться только в кошмарном сне. Как и два рабочих стола.
Два footer'а на одной веб-странице? No wai. Ловите извращенца.
В общем, к счастью для них, их говноподелия (3D API и операционные системы) обычно столько не живут, чтобы они столкнулись с подросшим синглтоном, который ставит жирный крест на развитии системы. А уж про говносайтики что говорить — они как бабочки-однодневки, умирают каждый день.
Но к несчастью для нас, синглтонный подход («что-то какого-то типа может/должно быть только одно») прописался во многих стандартах, и живее всех живых. Например, HTML использует привязку по идентификаторам в куче подстандартов (один из них — WAI ARIA для поддержки незрячих юзеров), что не даёт шаблонизировать разметку без такой-то матери.
Вот с CoCreateInstance неудачный пример. Нет никаких проблем вызвать ту функцию два раза, передав ей разные параметры. Проблемы могут наблюдаться как раз у CoCreateInstance, ведь в этом способе создания объектов параметров конструктора не предусмотрено вовсе.
Понимаю, о чём вы, но это чисто техническое ограничение, для обхода которого придумали паттерн two stage creation. Если помните, так же были устроены многие обёртки над WinAPI. Нет никакой проблемы сделать метод Init()
, или Create()
, который принимал бы все нужные параметры.
Но зачем?.. Чем двухэтапное создание лучше простой функции? Единственное что я вижу - это возможность включить конструктор в TLB.
Двухэтапное создание делается не от хорошей жизни. Это в целом вещь неочевидная и грозящая проблемами в духе "начали юзать до завершения второго этапа" или "надо везде проверить второй этап вызвали или нет", но иногда по другому ещё сложнее.
И чем же вызов функции так сложен?..
Вызов функции не сложен. Но двухфазная инициализация используется обычно в тех случаях когда ты не можешь две фазы склеить в одну: то есть между вызовами будет что-то ещё происходить. И вот это "что-то ещё" открывает дорогу багам
Но зачем?.. Чем двухэтапное создание лучше простой функции?
Так проблема-то не в функции, а в том, что нельзя было создать два директикса одновременно, чтобы управлять двумя картами сразу. Синглтон.
Ну так я о том и говорю, проблема вообще не в функции и не в CoCreateInstance.
Проблема в синглтоне, как таковом, а то, что они нарушили собственные правила, запретив инстанцирование встроенными средствами — характерный симптом этой проблемы.
Автор предъявляет слишком странные требования порой, а реально нужные случаи использования просто упускает.
А можно пример реально нужного случая? Тот, который по другому не решить (например через DI единственного экземпляра)
Например, класс, загружающий картинки/иконки, содержащий кэш. В любом случае этот объект будет один, и создавать их несколько вредно. (Только если по видам картинок, если вдруг надо.)
Или класс, обслуживающий доступ к базе данных, держащий пул коннектов к этой базе.
Или некий класс в P2P-мессенджере, держащий соединения с другими нодами.
Таких примеров может быть больше, и в любом случае такой синглтон будет явно один.
И проблемы с инициализацией из разных потоков, например, можно обойти, создав такой объект на раннем этапе, до старта дополнительных потоков...
Ну давайте подумаем.
Хотите ли в тестах продолжнать использовать уже загрязненный кэш вместо создания нового объекта?
А не появится ли необходимость запустить два логических экземпляра объекта в одном процессе, чтобы у них были свои кэши? (3 года назад ботоделы меня убеждали что не нужно такое никогда, а потом у половины из них "мультиботы" в ходу оказались)
Про пул соединений тоже классика - сгодня он один, а завтра завезли CQRS и у нас два пула: на чтение и на запись.
Но я отвлекся, вопрос был: чем синглтон в этих случаях ЛУЧШЕ других подходов, а не почему он может прокатить в частном случае
Ответ очевиден, синглтон лучше тем, что не требует передачи контекста/себя. Вот вы сосласиь на DI, а разве инжекция не требует того самого ну этого как же его, а вот синглтона?
Есть способы обойти ВСЕ перечисленные проблемы и остаться синглтоном, ну собственно DI это и подтверждает, но можно и самому сделать если надо.
Вот когда появится потребность мульти ботов или сикуреэс, вот тогда и перепишем с синглтона на 2(М) синглтонов просто разного типа. А с тестами все не просто, а элементарно - сделайте уже наконец функцию резет вашему синглтону или ещё лучше реализуейте сэтабл синглтон и тогда вот это поворот можно даже хоть мокать хоть фэйкать его как угодно.
А синглтонов в коде полно, например ОС апи - синглтон хоть и явно никакого объекта не требует, логирование, стандартные консольные потоки ввода/вывода, ФС, реестр и т.д.
Синглтон надо уметь готовить, а не критиковать. Если чего то стадо действительно больше одного ну просто уйдете от синглтона позже, да может быть тяжело и больно, но не ужас ужас ужас как пытается об этом сказать автор.
Так DI единственного экземпляра это и есть синглтон, классический паттерн (даже потокобезопасный) не очень широко применяется сейчас.
классический паттерн (даже потокобезопасный) не очень широко применяется сейчас
И почему же он не очень широко применяется? А потому что он - зло. Статья именно про него.
На самом деле проблема классического паттерна основная одна - это невозможность нормально замокать/протестировать в юнитах, поэтому создание синглтонов отдали на откуп DI, оставив всякие двойные проверки лока и вложенные классы со статическим конструктором (для потокобезопасности) учебным курсам.
И почему же он не очень широко применяется?
Потому что мало где нужен, да и сейчас повсеместно DI контейнеры, пишешь что-то вроде container.AddSingleton<MySingletonService>, а вся возня за тебя уже сделана.
Я работал на проектах где были те самые настоящие синглтоны, с проверкой на нулл и локом, проблем не было вообще никаких.
Проблемы там были постоянные, и заключались прежде всего в том, что такой синглтон не принимает параметров, в из-за чего все параметры оказывались в конфиге. Итогом чего были монструозные конфиги, которые приходилось прогонять через шаблонизатор чтобы хоть как-то разделить те настройки, которые крутить можно, от тех что крутить нельзя.
Наверное это был тот случай когда синглтон был не на своем месте.
И таких случаев в старом коде полным-полно. Как раз из-за мышления "объект всего один - значит синглтон - значит нужен паттерн".
А не является ли это обычным симптомом паттернофилии, когда вместо анализа проблемы и возможных путей ее решения лепят пытаются лепить паттерны из умной книжки? Потому что паттерны это круто. И дело вовсе не в синглтоне.
Дети, запомните, синглтон это плохо. Пнятненько?
Мне показалось, или все беды в мире от синглтонов?
Глобальное потепление, получается, тоже своего рода синглтон?
Вот за что мне симпатично функциональное программирование, так это за то, что там все функции - синглтоны. Раз у них нет состояния, то на всё приложение нужна ровно одна функция, выполняющая конкретную работу. И никто её демоном не считает.
Ну и даёт Maximiliano Contieri жару в универе Буэнос-Айреса!!
Мне кажется, что чистые функции и синглтон это всё-таки разные понятия
Функции не синглтоны, потому что существуют замыкания.
А вот вы не правы. Бывает, что функции существуют и без замыканий. Например, в C.
мы всё ещё про функциональное программирование говорим?
Да.
на си? без замыканий?
Да. А что вас смущает?
Я не ФП программист, но мне казалось, один из основных принципов ФП - неизменяемые переменные. С этим у си немного так проблемы. Ну и гарантий оптимизации хвостовой рекурсии тоже нет, что ограничивается в применении определенных фп-алгоритмов
Ну так вы сами не изменяйте переменные - и все дела. ФП - это прежде всего стиль написания кода (неизменяемость данных и минимизация побочных эффектов). То, что Си не очень подходит для ФП, никак не аргументирует вашу реплику "Функции не синглтоны, потому что существуют замыкания". Функции не синглтоны не поэтому. Как говорится, "слово - не воробей, да и вообще ничего не воробей, кроме воробья" (с)
Автор оригинального поста, Maximiliano Contieri, говорит про синглтон в контексте паттернов проектирования ООП, а так-то у термина "синглтон" значений слегка больше, чем одно. Например, жизненный цикл зависимостей в DI контейнерах тоже может быть синглтоном. Поэтому утверждать "Синглтон - корень всех зол!" несколько кликбейтно. А если вам внимание важнее понимания, то вот и получите. Он про синглтон, и я про синглтон. Может у него в ООП синглтон и корень всех зол, но в ФП вообще такого понятия нет. Вернее, там все функции - одиночки. Нужны ровно в одном экземпляре.
не изменяйте переменные и все дела"
Ну давайте решим простую задачу в функциональном стиле: пользователь вводит число N и потом N чисел, надо их отсортировать и вывети. Как это реализовать на Си без любых изменений переменных? Сортировку можете не писать, давайте её держать в голове как готовую (функция принимает массив, возвращает массив), ограничимся только вводом данных. Как это будет выглядеть на си в ФП стиле?
жизненный цикл зависимостей в DI контейнерах тоже может быть синглтоном.
Он так называется по тем же причинам что и паттерн синглтон, но это принципиально разные вещи. Вы возможно не поняли, но статья про паттерн синглтон, а НЕ про возможности DI-контейнеров
Под тем, что функция не синглтон я имел ввиду, что не любая функция существует в одном экземпляре. Функциональное программирование активно использует замыкания, поэтому там функции пересоздаются постоянно с разными состояниями. То, что в ООП выглядело бы как класс с методом и атрибутами-данными
Ну давайте решим простую задачу в функциональном стиле: пользователь вводит число N и потом N чисел, надо их отсортировать и вывети. Как это реализовать на Си без любых изменений переменных? Сортировку можете не писать, давайте её держать в голове как готовую (функция принимает массив, возвращает массив), ограничимся только вводом данных. Как это будет выглядеть на си в ФП стиле?
так изи же
char* transform(char* in, int len) {
char* result = malloc(sizeof(char) * (size_t)len);
for (int i = 0; i < len; i++) {
result[i] = do_something(in[i]);
}
return result;
}
никаких изменений глобального состояния нет. Зато памяти жрет больше
вижу изменение: `result[i]=do_something`. Пожалуйста, продемонстрируйте код, где не будет изменяемых переменных
поправка: тут меняется не сама переменная, а область памяти, на которую она указывает + смещение. В целом можно сделать через malloc/memcpy аллокацию нового массива и копирование памяти в него с добавлением элемента, но никто в здравом уме и твердой памяти так делать не будет, ибо это грозит стрельбой себе в колено из пулемета
не важно, что именно меняется. в ФП изменяемые объекты/переменные/сущности/области памяти не существуют. Реализуйте пожалуйста БЕЗ изменений чего либо, чтобы показать что на Си можно писать в ФП стиле
ну кок
char* transform_inner(char* src, size_t max, char *tmp, size_t len) {
char* result = malloc(sizeof(char) * len+1);
if (tmp != NULL) {
memset(result, tmp, len);
free(tmp);
}
result[len] = do_something(src[len]);
if (len == max-1)
return result;
return transform_inner(src, max, result, len+1);
}
char * transform(char * orig, int len) {
return transform_inner(orig, len, NULL, 0)
}
надеюсь не ошибся нигде, и про строчки с 3 по 8 можете не писать - ваш ФП-язык делает это под капотом, только free(tmp) делает сборщик мусора
з.ы. можно было бы конечно еще на тему ленивых вычислений в СИ поразмышлять, но мне лень
все ещё вижу `result[len]=...` - модификация уже созданной выше структуры. (Я бы ещё и на free(tmp) поворчал - мы так меняем "состояние" блока памяти, а он мог где-то использоваться). Пожалуйста, напишите код так, чтобы после первоначальной инициализации с переменной больше ничего не происходило, никак её память не менялась вами в коде, иначе это не ФП код.
Суть ФП подхода в том что изменяющие операции там отстутвтуют на уровне логики кода. Компилятор конечно же их реализует сам, но в этом и суть - вы не делаете их вообще, это находится ВНЕ вашего кода
еще раз. Ваш ФП-язык это делает под капотом. Нельзя одновременно аллоцировать память и писать в нее, эти операции всегда будут идти последовательно. Просто ваш ФП-язык делает это неявно, а ввиду низкоуровневости сишки эти подкапотные вещи надо писать руками. Я могу написать вот так:
// Подкапотная функция добавления в массив,
// которая вшита в компилятор/интерпретатор вашего
// ФП-языка. Реализация там будет чуть отличаться
// но суть та же самая
// - аллокация памяти
// - копирование оригинального массива
// - запись добавленного элемента
char* array_push_internal(char* arr, size_t len, char new_lem) {
char* result = malloc(sizeof(char) * len+1);
if (tmp != NULL) {
memset(result, tmp, len);
free(tmp);
}
result[len] = new_elem;
return result;
}
char* transform_inner(char* src, size_t max, char *tmp, size_t len) {
char* result = array_push(tmp, len, do_something(src[len]));
if (len == max-1)
return result;
return transform_inner(src, max, result, len+1);
}
char * transform(char * orig, int len) {
return transform_inner(orig, len, NULL, 0)
}
Именно что Под капотом. Речь шла не о том, чтобы реализовать на си код, идентичный результату компиляции ФП кода, а о том чтобы писать в ФП стиле. Ваша подкапотная магия некорректна, так как не контролирует сейчас наличие указателей на tmp
. Боюсь чтобы в ФП стиле писать на Си, надо ОЧЕНЬ много ещё реализовать, а потом желательно ещё обвеситься линтерами которые будут проверять что вы не используете стандартные функции си вне этих мест
если добавить ФП в вашем понимании в СИ - это будет не СИ, а что-то другое. Фишка си как раз в его низкоуровневости. Вы еще на ассемблере предложите писать в ФП-стиле, во хохма-то будет
З.Ы. а вообще можно сделать и так, если функцию вставки в массив написать на ассемблере или на том же си и прилинковать отдельно. Тогда у вас будет ваш ФП-стиль, добавление же под капотом будет работать
Я не предлагал писать на Си в ФП стиле. Мне это как раз кажется сомнительной идеей - нужно переизобрести инстурментарий и запретить стандартный. Но почему-то человек выше сделал вид, что все нормально.
Речь шла не о том, чтобы реализовать на си код, идентичный результату компиляции ФП кода, а о том чтобы писать в ФП стиле
путаетесь в показаниях, уважаемый
Человек выше на Си писал последний раз лет 15-20 назад. Если вам это действительно интересно, можете поговорить с ChatGPT - https://chatgpt.com/share/678c07b7-e930-800d-92c5-e92d9d800daa Он там что-то такое выдал и умно рассуждает. Можете его переубедить.
"Функции не синглтоны, потому что существуют замыкания." (с)
Мне кажется, что вы пытались донести до меня мысль "В ФП не все функции синглтоны, потому что существуют замыкания".
Вот если бы вы свою мысль оформили таким образом, я бы, возможно, и не триггернулся. А если бы автор поста не назвал его "Синглтон - корень всех зол", я бы и на статью не триггернулся. Ну, как есть.
А так-то я ни в ФП ничего не понимаю, ни в программировании на С. Высказал свою точку зрения, получил фидбек. Всё норм - это же Хабр. За этим я и здесь.
Человек выше на Си писал последний раз лет 15-20 назад.
15-20 лет назад я еще пешком под стол ходил, можно сказать, просто у меня опыта на си не так много, и все-таки речь шла про массив, а не связный список (тут-то как раз понятно что как реализовать)
Вот вариант для массивов от ИИ. Я на Си не думаю, поэтому не могу сказать, что там лажа, а что нет.
код для массивов от ChatGPT
#include <stdio.h>
#include <stdlib.h>
/*
* Рекурсивная копия массива:
* copyArray(src, n, dst, offset) копирует n элементов из src в dst,
* начиная с dst[offset], не меняя src.
*/
void copyArray(const int *src, int n, int *dst, int offset) {
if (n == 0) {
return;
}
dst[offset] = src[0];
copyArray(src + 1, n - 1, dst, offset + 1);
}
/*
* insertIntoSortedArray(a, n, x) возвращает указатель на *новый* массив
* длины n+1, куда вставлено число x в нужное место, предполагая что
* исходный массив a (длины n) уже отсортирован. Старый массив a не меняется.
*/
int* insertIntoSortedArray(const int *a, int n, int x) {
if (n == 0) {
// Новый массив из одного элемента
int *res = malloc(sizeof(int));
res[0] = x;
return res;
}
if (x < a[0]) {
// x становится в начало нового массива, дальше копируем a
int *res = malloc((n + 1) * sizeof(int));
res[0] = x;
copyArray(a, n, res, 1); // старый массив копируется со сдвигом
return res;
} else {
// Первый элемент нового массива совпадает со старым a[0],
// а x вставляем рекурсивно в "хвост" (a+1)
int *res = malloc((n + 1) * sizeof(int));
res[0] = a[0];
int *temp = insertIntoSortedArray(a + 1, n - 1, x);
copyArray(temp, n, res, 1);
free(temp);
return res;
}
}
/*
* readSortedArray(n) считывает n чисел и возвращает указатель на
* отсортированный массив длины n. Реализовано рекурсивно:
* - если n==0, возвращаем NULL (пустой массив);
* - иначе читаем одно число x, рекурсивно получаем массив из (n-1) чисел
* (уже отсортированный) и вставляем в него x, возвращая новый массив.
*/
int* readSortedArray(int n) {
if (n == 0) {
return NULL;
}
int x;
scanf("%d", &x);
// Рекурсивно получаем отсортированный массив из n-1 элементов
int *arr = readSortedArray(n - 1);
// Вставляем новое число x в нужное место
int *res = insertIntoSortedArray(arr, n - 1, x);
free(arr); // старый (короче на 1) уже не нужен
return res;
}
/*
* Рекурсивный вывод массива:
* printArray(a, n) печатает n чисел массива a.
*/
void printArray(const int *a, int n) {
if (n == 0) {
return;
}
printf("%d ", a[0]);
printArray(a + 1, n - 1);
}
int main(void) {
int n;
scanf("%d", &n);
// Считываем n чисел и получаем новый (отсортированный) массив
int *sorted = readSortedArray(n);
// Печатаем итог
printArray(sorted, n);
printf("\n");
// Не забудем освободить итоговый массив
free(sorted);
return 0;
}
Просто человек выше очень сильно намекает, что на С нельзя написать что-то в функциональном стиле из-за того, что "у си немного так проблемы" с неизменяемыми переменными, да "и гарантий оптимизации хвостовой рекурсии тоже нет". А ChatGPT говорит:
С теоретической точки зрения, оба языка (Haskell и C) тьюринг-полны. Это значит, что любую задачу, формально решаемую на одном языке, в принципе можно решить и на другом. Соответственно, нет «таких программ, которые можно написать на Haskell и невозможно написать на C».
Возможно, что ChatGPT неправ, а возможно, что человек выше думал написать одно, а написал другое. А возможно и я чего-то там не так понял. Например то, что он таким образом опроверг мой тезис "Бывает, что функции существуют и без замыканий. Например, в C" Я-то точно помню, лет 15-20 назад функции в С были, а замыканий не было. Если что-то за это время поменялось и теперь в Си функции без замыканий не существуют - ребята, я просто не успел за прогрессом!!
Вам ИИ привёл типичный процедурный код, а не функциональный. Не обманывайтесь похоже звучащими словами: то что в Си процедуры называются функциями никак не превращает процедурное программирование в ФП.
Меня давным-давно учили, что процедура от функции отличается только тем, что функция возвращает результат, а процедура - нет. Но это было лет 30 назад и на Pascal'е.
Т.е., вы считаете, что функциональное программирование возможно только на специально созданных для этого языках? Что на Си нельзя писать в парадигме ФП? Я уже говорил, что я в ФП не очень глубоко погружался (у меня с абстрактным мышлением проблемы, а ФП - это математика, а математика - это абстрации), но у меня сложилось впечатление, что для использования ФП не нужны "специальные ЯП". И тут вы заявляете, что в Си нет функций, там только процедуры, а процедурами в ФП нельзя. Я в сомнениях, вы рушите мою картину мира.
Функция от процедуры и правда отличается только возвращаемым значением, а вот функциональное и процедурное программирование - это две разные парадигмы.
При большом желании, конечно же, можно функциональную программу записать на любом языке, но при этом придётся закатить солнце вручную столько раз, что с практической точки зрения единственной достойной причиной так делать является написание статьи в хаб "Ненормальное программирование".
Так что да, на Си нельзя писать в функциональной парадигме. Сильно мешает отсутствие замыканий и сборщика мусора.
Что в этом коде на Си указывает на то, что он именно процедурный, и в каком месте он не соответствует функциональной парадигме?
int add(int a, int b) {
return a + b;
}
Да, это простой пример. Это очень простой пример. И для очень простого примера должен быть очень простой ответ на мой вопрос.
В этом - ничего, он слишком простой.
Вот и я о том же. ФП - это способ создания программ. Способ мышления при использовании инструмента. ООП - другой способ мышления. Отвёрткой можно забивать гвозди, а вот закручивать шурупы молотком не получится, но можно шурупы молотком забивать.
Да, я согласен, что одни инструменты лучше подходят для выполнения определённых задач, а другие хуже. Наверное, именно поэтому я, в конце-концов, программирую на JS - на нём и шурупы можно отвёрткой закручивать, и гвозди молотком забивать. На нём же, при достаточном желании и упоротости упорстве можно и шуруп отвёрткой забить. Насчёт закрутить гвоздь молотком - сильно сомневаюсь. Я б не осилил.
Попробуйте код чуть посложнее - напишите функцию поиска максимума в ФП стиле
А в чем сложность?
https://godbolt.org/z/cPrs55f7j
Во всём.
Во-первых, эта функция заточена на инты и массивы. Максимум может быть не только среди интов, не только в массивах (но и в мапах, например, или даже в optional'ах), да и хотелось бы уметь передавать произвольный предикат, чтобы хотя бы немножко почувствовать эту самую Ф при П. Ну, что-то вроде такого:
maximumBy :: Foldable t => (a -> a -> Ordering) -> t a -> Maybe a
Во-вторых, тип вашей функции показывает, что она скорее всего не будет работать для пустых массивов. И правда: array[0]
ой сегфолт (в лучшем случае).
Первый тейк - относится не к невозможности писать в функциональном стиле, а что в сях не очень с полиморфизмом(обойдёмся без void*), и эти требования как то выше не предполагались. Хотя я на функциональных языках не программировал, мб это там повсеместно.
А второе - да, ошибся, код написал за 3 минуты.
я взял небольшой массив размером 1000000 чисел (это же 4мб памяти, да?) что-то падает, не подскажете почему?
Первый тейк - относится не к невозможности писать в функциональном стиле, а что в сях не очень с полиморфизмом(обойдёмся без void*), и эти требования как то выше не предполагались. Хотя я на функциональных языках не программировал, мб это там повсеместно.
Это там именно что повсеместно. По крайней мере, в ML-семействе.
А второе - да, ошибся, код написал за 3 минуты.
Это как раз норм (для C++), потому что C++ и C не приучают думать в терминах типов. Функциональщик начал бы с написания сигнатуры и автоматически подумал «а как я инт-то верну на пустых входных данных? надо в Maybe завернуться».
в си вроде как замыканий нет, но в 11 стандарте, если склероз не изменяет, добавили указатели на функции, но это все равно не замыкание в полном смысле этого слова
Он привносит ненужные ограничения в ситуациях, где единственный экземпляр класса на самом деле не требуется, а также вводит в приложение глобальное состояние.
после этой фразы можно дальше не читать
наверное, вы просто не знаете чего хотите или рандомно решаете, что будет синглтоном, а что нет..
В анемичной модели с сервисами зачастую подвязанные через DI сервисы - это определенные как single instance сущности, и никакого смысла их делать чем-то иным нет.
Всё так, поскольку сервисы не имеют своего стейта. Это по сути молотилка в которую с одной стороны загружают данные, с другой получают другие данные. Автор же похоже рассуждает об этих данных, которые могут быть представлены объектами ( объекты данных? или как их назвать?). У них конечно есть стейт и в общем случае каждый из них уникален и конечно никакие они не синглтоны.
Синглтон — корень всех зол
Только Ситхи все возводят в абсолют
Spring и все что он принес в массы вообще мимо вас прошло?
Почему не стоит использовать:
5. Не экономится пространство в памяти
12. Накапливается мусор, занимающий место в памяти
Когда может пригодиться:
1. Этот паттерн помогает экономить память
Это ещё корпускулярно-волновой дуализм, или уже диссоциативное расстройство личности (синглтона)?
MemoryCache в C# является синглтоном, прекрасно внедряется через DI и прекрасно контролируется в тестах. И как им нормально пользоваться если бы он не был синглтоном, я не очень представляю.
А в чем профит того что он синглторн если он везде внедряется через DI? В чем отличие от не-синглтона?
И как им нормально пользоваться если бы он не был синглтоном, я не очень представляю.
Просто создаете его вызовом new MemoryCache(...), сохраняете ссылку на него (например, в поле своего объекта, реализующего какой-то другой сервис с временем жизни Singleton), и пользуетесь. Это разумно делать там, где нужны другие свойства кэша, такие, как автоматическое удаление устаревших объектов. Преимущество своего кэша - в том, что его можно создать с другими параметрами, например - с автосжатием по достижении определенного размера. Иногда, хоть этот размер и меряется в попугаях, это полезно. С общим же для приложения кэшем - реализацией singleton-сервиса IMemoryCache - такое делать нельзя, потому что для использования автосжатия надо указывать, сколько попугаев в каждом размещаемом в кэше объекте, а другие пользователи общего кэша этим не заморачиваются.
Читал я код от автора, начитавшегося такой фигни. У него нет синглтона. У него есть объект, создающийся в main() и потом указатель на этот объект входит в состав 4/5 всех классов программе и передаётся в каждый конструктор.
А проблема в чем?
Проблема в том, что в программе всё равно есть объект, существующий в одном экземпляре, потому что он нужен по логике. К нему точно так же получают доступ ото всюду. Но кроме этого теперь есть ещё и сотни указателей, которые нужно вручную копировать туды-сюды. То есть стало только хуже, но зато не используется ужасный синглтон с методом get_instance()
Вы тут немного путаете. Когда ссылка на объект внедряется в качестве зависимости через конструктор - это не синглтон, т.к. никто не мешает в один экземпляр передать ссылку на один объект, в другой экземпляр ссылку на другой объект. Синглтон же подразумевает, что в системе возможен только один экземпляр и доступ к нему имеют все через глобальное состояние.
Внедрение зависимости как раз является примером того, как в системе можно иметь один объект, но при этом не накладывать на него всех минусов синглтона. Например: не создается проблем с тестированием; нет точки куда ходят все части программы; объект выполняет только свою задачу и при этом не занимается контролем того, что он в системе один
Зато теперь этот объект можно нормально сконфигурировать перед использованием, больше нету гонки между конфигурацией и использованием.
Простите, но между "вручную копировать" и "вручную искать где же юзается get_instance раньше чем надо" я выбираю первое. Это точно проблема?
Я выбираю сразу написать по моему главному правилу: невалидное состояние системы непредставимо в системе типов. Если синглтон надо конфигурировать, и это нельзя просто сделать в самом get_instance, то get_instance будет возвращать объект АМожетБытьСинглтон, который надо будет проверить на фактический тип, чтобы получить доступ к возможностям синглтона. (Как именно и какие накладные расходы - уже зависит от языка).
если синглтон надо конфигурировать, значит каждый get_instance должен принимать ВСЮ конфигурацию, иначе мы не знаем с какой конфигурацией будет создан экземпляр.
Я выбираю сразу написать по моему главному правилу: невалидное состояние системы непредставимо в системе типов.
Только вы им не пользуетесь.
Когда у вас есть static optional<Singleton> instance()
и есть какой-то class SingletonUser
, который без синглтона жить не может, но конструктор которого не требует синглтона, то у вас в ваших типах представимо состояние «синглтон не создался, а SingletonUser
создался», что ведёт к боли, страданию и проверкам внутри каждого такого SingletonUser
, действительно ли синглтон создался.
Когда у вас нет никакого static optional<Singleton> instance()
, а есть конструктор
class SingletonUser
{
public:
SingletonUser(Singleton& instance);
};
то у вас состояние «синлтона нет, а клиент синглтона есть» непредставимо, как вы и хотели.
И вам даже нужно меньше проверок, потому что вы эти проверки делаете один раз и условно в main()
вместо того, чтобы их делать каждый раз в каждом клиенте синглтона.
Ну почему же так радикально? Например опшионал может быть нужен только для тестов, а не в продакшене, тогда нет нужды ни в каких проверках, а если все таки вдруг кто то что поломал в продакшене и синглтон пуст при первом использовании то доступ к значению опшионала кинет исключение или терминирует программу кому как нравится.
Если же опшионал на самом деле нужен в продакшнене то ваша замена с конструктором не эквивалентна т.к. проверку на пусто кто то где то всё ж таки должен делать.
Например опшионал может быть нужен только для тестов, а не в продакшене, тогда нет нужды ни в каких проверках
Почему? Кто мне это в типах гарантирует? Это ведь сломается при малейшем рефакторинге или изменении требований.
Вообще какое-то странное требование, чтобы в тестах нужно было, а так — нет. Можно какой-нибудь пример, чтобы я лучше понимал контекст?
то доступ к значению опшионала кинет исключение или терминирует программу кому как нравится.
А если вы пишете на плюсах (мои искренние соболезнования)? Как там, что у нас operator*
и operator->
говорят про пустой опшонал? А, во:
The behavior is undefined if *this does not contain a value.
Всё как всегда в плюсах.
Если же опшионал на самом деле нужен в продакшнене то ваша замена с конструктором не эквивалентна т.к. проверку на пусто кто то где то всё ж таки должен делать.
Я и говорю, что эта замена не эквивалентна, а лучше: проверку нужно делать один раз и в условном main()
, а не размазывать её по всей кодовой базе с неясной семантикой на пару.
Можно какой-нибудь пример, чтобы я лучше понимал контекст?
Ну например вы реализуете сэтабл синглтон. В продакшеге у вас один код который его сетит и он всегда исполняется при инициализации, а в тестах вы резетете его после/перед каждым тестом. Да конечно можно и функцию резет в самом типе реализовать, но она не нужна в продакшнене, а опшионал ее и так реализует.
Я как раз на плюсах и пишу там помимо * и -> есть еще и валью и валью_ор и в 23 ещё и прочие функциональные зены и элсы подвезли. Так что все там есть чтобы писать без уб. Но уб наше все конечно куда ж без него. Я чиню говнокод и на плюсах (прод) и на питоне (авто тесты) и в ява скрипте (кумл/гуй). Конечно плюсы 99% моего времени, но я не заметил каких то принципиальных отличий ну опять же кроме самого важного для плюсов - весь спектр уб. Ну т.е. да на плюсах гавнокода я вижу сильно больше, но он это интересней что ли.
А с проверкой один раз в мэйне может не получится опять же если поодакшен по бизнес логике требует опшионал, а если не требует то и проверять явно ничего не надо, а вместо этого использовать функции/апи без уб.
В продакшеге у вас один код который его сетит и он всегда исполняется при инициализации, а в тестах вы резетете его после/перед каждым тестом. Да конечно можно и функцию резет в самом типе реализовать, но она не нужна в продакшнене, а опшионал ее и так реализует.
А если бы вы просто имели параметр в конструкторе, то вам бы не пришлось ничего ресетить: вы просто создаёте новый объект на каждый новый экземпляр класса. Десинглтонизация снова выигрывает!
Я как раз на плюсах и пишу там помимо * и -> есть еще и валью и валью_ор и в 23 ещё и прочие функциональные зены и элсы подвезли.
Запретили уже стайлгайдом или линтером *
и ->
?
Синглтоны — это ссылки, прикреплённые к классам. Поскольку классы являются глобальными ссылками, сборщик мусора их не затрагивает. В случае, если синглтон является сложным объектом, то этот объект будет оставаться в памяти на протяжении всего процесса выполнения, а вдобавок будет транзитивно замыкать все свои ссылки.
Если вы знаете, что синглтон вам больше не нужен, то что мешает добавить в него метод, обнуляющий ресурсы?
В типичном приложении у нас один HTTP клиент и один коннект к базе данных. Нет никакого смысла делать их несколько. Просто в современном программировании эти синглтоны пробрасываются через DI и клиент не создает их самостоятельно.
А в чем проблема сделать тестовый friend класс для загрузки testdouble?
Этот аргумент не выдерживает критики
По-моему статья не выдерживает критики, синглтоны нужны (тот же пул коннектов с БД нам что, при обработке запроса каждый раз заново создавать? Да и что автор подразумевает под синглтоном?)
Тот же единичный объект, передающийся по ссылке в DI то же по сути синглтон, какая разница с точки зрения функционала передастся он ссылкой в параметр или получен через getInstance(). Читаемость кода это может и повысит, но не везде тот же DI используется, особенно если с легаси-кодом работаешь либо с поделками сумрачных гениев. Такое ощущение, что автор сам не до конца понимает, что такое синглтон
Да и что автор подразумевает под синглтоном?)
Общеизвестный порождающий паттерн, что же ещё он мог подразумевать?
Одиночка (англ. Singleton) — порождающий шаблон проектирования, гарантирующий, что в приложении будет единственный экземпляр некоторого класса, и предоставляющий глобальную точку доступа к этому экземпляру.
Пул соединений, создаваемый при запуске один раз, и прокидываемый через DI тоже будет синглтоном, и такой объект проще переиспользовать, чем каждый раз создавать заново. Автор явно перебрал с галоперидолом и подогнал частный случай под общий
З.Ы. ну и говоря про частные случаи - если сервис по некоторым причинам может использовать только in-memory кэш, то такой кэш не сможет быть не синглтоном - иначе он просто работать не будет
Не обязательно будет. Я уже приводил пример необходимости двух пулов: для чтения (в реплики) и для записи (в мастер)
ну будет два синглтона (причем которые можно запихать в один), суть не меняется
Синглтон это когда один экземпляр. Почему у вас два?
Усложним, у нас приложение стало модульным монолитом (мы пока не хотим микросервисы, но готовимся). В каждом ограниченном контексте тоже есть пул соединений. Уже стало и не два, выходит.
type DbSingleton struct {
rpool *mydriver.ConnPool
wpool *mydriver.ConnPool
}
func (s *DbSingleton) Reader() *mydriver.ConnPool {
return s.rpool
}
func (s *DbSingleton) Writer() *mydriver.ConnPool [
return s.wpool
]
ну я ж написал, что можно в один объект завернуть, и будет вам синглтон
Так погодите, выше утверждалось, что пул - синглтон, а теперь он как раз и не синглтон.
не надо демагогии, я писал про пул, работающий в обе стороны. Если вам нужно два пула с разделением чтения и записи ничего не мешает завернуть из в синглтон, показанный выше. Это во-первых. А во-вторых, если пулы обращаются к разным репликам (а иначе нахрена разделение делать), то тут они хоть и будут иметь один тип, но функционально это будут два отдельных объекта
Если для вас слова "то что считалось синглтоном перестало им быть" - демагогия, но я не знаю как ещё донести мысль. Было исходное утверждение "Тип ХХХ - синглтон", в процессе обсуждения мы таки выяснили, что нужны 2 его экземпляра (или больше). То есть исходное предположение было неверно. С синглтонами всегда так: думаешь что это железобетонно одна штука, а потом выясняется что иногда и нет. И сиди бегай все get_instance()
ищи.
то тут они хоть и будут иметь один тип, но функционально это будут два отдельных объекта
вот эту часть не понял. Паттерн синглтон именно про отсутвтвие нескольких экземпляров одного типа
а потом выясняется что иногда и нет.
Если такое выясняется раз в неделю, то у вас что-то не так проектированием архитектуры, и отсутствие или наличие синглтонов там вряд ли поможет
Пул соединений, создаваемый при запуске один раз, и прокидываемый через DI, не будет порождающим паттерном "Одиночка". Просто потому что этот самый порождающий паттерн подразумевает совсем другой способ получения ссылки на объект, отличный от прокидывания через DI.
что же ещё он мог подразумевать?
Да любой объект, принципиально существующий в программе в одном экземпляре, ссылки на который выглядят как независимые объекты. Например - сервисы в контейнере сервисов, получаемые из этого контейнера как ссылки на единственный реализующий их экземпляр. И вместо того, чтобы долго говорить/писать "сервис с временем жизни, совпадающим с временем жизни контейнера" часто для обзначения таких сервисов используют просто одно слово "singleton". И, судя по комментариям, многие именно так автора и поняли. Тем более, что начало описания недостатков - например, в п.1 - полностью применимо и к такому синглетону.
То, что "синглетон" понимается чисто в смысле "порждающий шаблон Одиночка" из древней книги "четверки", при беглом просмотре неочевидно - ибо книга в тексте статьи и по имени не называется, и приводится не как источник определения, а как пример - короче, при беглом чтении эта отсылка проскакивает незамеченной.
Я вот тоже понял автора статьи именно так. И даже накатал полстранички комментария с несогласием (к счастью, вовремя понял, что речь у автора не о том). Потому что я, как и многие здесь - не преподаватель, и перечитывать и растолковывать тексты древних (30 лет уже!) книг в мою повседневную работу не входит. А на практике я использование этого шаблона уж и не помню, когда видел. Потому что в том же современном .NET и С# есть куда более удобные для практического применения способы создания таких, единственных в рамках прогрограммы, объектов, чем были 30 лет назад.
Это хорошо, что вы не видели этот паттерн. Я видел и судя по другим комментариям, люди продолжают использовать. IoC-контейнеры же есть не у всех и не у всех скоуп синглтон называется именно так (это классическое название, но все же). Так что когда говорят "синглтон", без уточнений ожидалсь бы, что речь именно о самостоятельной сущности, а не детали реализации какой-то библиотеки (пусть даже такой большой как спринг и аналоги). Второе ожидание, что люди знакомые с IoC-контейнеоами знают и паттерн и способных их отличить. Вот вы справились, а кто-то - нет, и мне кажется виноват не автор
Что же касается книги - возраст не повод её выкидывать. Книга все ещё актуальная и рекомендуется к прочтению всем кто переходит от уровень junior дальше.
Это хорошо, что вы не видели этот паттерн.
Увы, это не так. И "давно" не означает "никогда". И в JS можно хоть сейчас написать конструктор для класса, который вернет ссылку не на вновь созданный объект, а на нечто другое - на тот самый синглетон, к примеру (другое дело, что я не фронтовик, и JS мне нужен разве чтобы только сделать макет, который взаимодействует с моим тылом, чтобы проверить, что оттуда приходит).
Что же касается книги - возраст не повод её выкидывать.
Ну да. Хотя бы потому, что тамошние "паттерны" - это вполне себе годная основа для коммуникации: благодаря ей многие знают одни и те же приемы под одними и теми же назавниями. А так, не будь этой книги, сами приемы вполне учатся чисто на практике (но без общепринятых названий).
Большинство из доводов, написанные в том виде, в каком они написаны, легко опровергаются. Например, довод про якобы вынужденную многопоточность, и т.д.
А некоторые доводы, например как "доказано, что синглтоны ведут к ошибкам", просто откровенно врут - по ссылке нет ни слова про увеличение кол-ва ошибок.
Не стоит критиковать автора статьи. Просто скажу - не читайте это, - цените своё время. И мой комментарий не читайте ниже... Бессмыслица.
Человеку нужно было выразиться в тексте и он выбрал самую беззащитную тему - Синглтон. Я думаю такие статьи выйдут и в 2030-ом, и в 42-м годах. Как выходили в 2004-м и 14-м. И жук и жаба пишут эту чушь. Это вид спорта просто такой - писать про GoF.
Давайте вместе просто ему посочувствуем, похлопав дружески по плечу. Ведь человек попытался выразиться и у него это получилось: появились буквы и складный текст с картинками. Смысла только нет.
Я уже программирую на c# бэкэнд полтора года.Singleton действительно паттерн который в 99% не очень.Но есть исключения.1 - когда иметь больше одного экземпляра не имеет никакого смысла, 2 - когда нам очень важна производительность и мы не хотим несколько раз инициализировать поле.Пример - я сейчас делаю свой мессенджер.Сейчас я пишу бэкэнд.Во время разработки бэкэнда я использую различного рода системы защиты, одно из них хэширование с помощью sha512.Инициализирлвание экземпляра класса SHA512 с помощью метод Build является довольно критическим для производительности.Для того чтобы это решить я создал интерфейс IHashNetwork и обязываю всех наследников реализовывать метод Hash.Потом я создал статический класс HashUtil и создал поле IHashNetwork hashNetwork, потом инициализирую с помощью метода Build.Создал метод HashSHA512 и возвращаю результат метода Hash от hashNetwork.Такое решение является весьма оптимальным, потому что даже если мы например захотим добавить функционал то мы сможем создавать бесконечность классов IHashNetwork и просто их добавлять в общую хэш утилиту.1 - мы повысили производительность, 2 - упростили код.Итог - singleton можно использовать в некоторых ситуациях, но его нельзя назвать паттерном который ты скажешь первым делом на вопрос какие паттерны знаешь.
Патерн нужный, но как его большинство реализует это ужас.
А всего-то виртуальные конструкторы в язык добавить, да генерик написать, возвращающий одиночный объект.
Извините, но ошибки в синглтоне, описанные вами, могут возникать только в многопоточной среде, а это совершенно отдельная вселенная. Паттерн синглтон прекрасно работает.
Синглтон — корень всех зол