Pull to refresh

[Археология Live] Стыдный разговор о синглтонах

Reading time 8 min
Views 19K

Аудитория: Java Junior, любители холиворов, профессиональные написатели синглтонов



Любые замечания и предложения — очень приветствуются. Это мое первое видео, и не совсем понятно, нужен ли тут вообще такой контент. Считайте это закрытым альфа-тестом, только для посетителей хаба Java :)


Ниже дана полная текстовая расшифровка, кому не хочется тратить время на просмотр.


Вступление


Привет, Хабр! Наступил вечер, и нам пора серьезно поговорить. Хочу с тобой обсудить стыдное. Cинглтоны.


Дело в том, что я постоянно слышу от знакомых на попойках, как они на код ревью уничтожают синглтоны и выкашивают спринг, потому что Спринг – шлак. И вот сил моих больше нет терпеть, давайте заясню за всю фигню.


Сначала о том, почему синглтоны – это стыдно.


Старейшая книга в которой говорится о синглтоне (ну по крайней мере, самая старая, какую видел своими глазами) написана в 1994 году. Это книга «Паттерны проектирования» Банды Четырех: Гамма, Хелм, Джонсон, Влиссидес.
Просто задумайтесь, какая это древность. Чем вы занимались в 1994 году? Кое-кто из наших коллег в этом году еще не родился.



Или вот, вторая любовь моей жизни – книга «Test Driven Development» Кента Бека, написанная в 2002 году.



И вот что написано про синглтоны там:



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



Поэтому в принципе, мне кажется, что обсуждать на публике синглтоны – это стыдно. Об этом можно поговорить только с самыми близкими друзьями, со своей любимой женщиной/мужчиной, ну и с тобой, дорогой Хабр.


Светлая сторона


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


Глобальное состояние


Синглтон — это в первую очередь глобальное состояние. Вы получаете один неделимый, плохо управляемый глобальный скоуп. Этот недостаток управляемости – сама по себе проблема.


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


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


Явное и неявное


Существует известный принцип, согласно которому явное лучше неявного. Этот принцип, кстати, заложен в PEP 20, более известный как "дзен языка Python".



Когда ты передаешь объекты не точно по адресу, а переносишь их в глобальное состояние, этим самым ты прячешь связи внутри кода. Это супер неявно. В результате, код быстро превращается в магию. Он как-то работает, но сам по себе, и он уже не в твоей власти.


The Single Responsibility Principle


Принцип единственной ответственности. То есть это буква S в аббревиатуре SOLID. Эта аббревиатура – важнейшая в жизни любого джависта. Я так ее уважаю, что хочу сделать наколку с ней.


Этот принцип когда-то ввел Роберт Мартин (более известный как Дядя Боб).



Он утверждает, что каждый объект должен иметь одну ответственность и эта ответственность должна быть полностью инкапсулирована в класс.


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


Сильная связанность


Все компоненты, использующие синглтон, или не дай бог – общающиеся через него, мгновенно оказываются жестко связанными. Связанное зачастую приходится развязывать — например, чтобы написать тесты. И, тестировать код, густо обмазанный синглтонами, очень неприятно. Даже один-единственный синглтон может серьезно напрячь вообще всех: и тестировщиков, и программистов с TDD, и подгадить на проведении демонстраций изолированной функциональности.


Специфика Java


В Java нет специального способа записывать синглтон. Поэтому существует множество способов записать его, и все они некрасивые.


Во-первых, все можно впихать в статическое поле или енум.


public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
}

public enum Singleton { INSTANCE; }

Но это будет не ленивый вариант, он нам не нужен.
Хотя енум таки можно сделать ленивым, если хочется.


Можно засунуть всё под synchronized:


public class Singleton {
    private static Singleton instance;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Но это будут тормоза, кои собственно и символизирует synchronized, в самом типичном случае использования.


Или приходится использовать double-checked locking вариант:


public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        Singleton localInstance = instance;
        if (localInstance == null) {
            synchronized (Singleton.class) {
                localInstance = instance;
                if (localInstance == null) {
                    instance = localInstance = new Singleton();
                }
            }
        }
        return localInstance;
    }
}

Выглядит он еще более мерзко. Плюс отношение к этой форме записи выразили сами разработчики языка Java:


"There exist a number of common but dubious coding idioms, such as the double-checked locking idiom, that are proposed to allow threads to communicate without synchronization. Almost all such idioms are invalid under the existing semantics, and are expected to remain invalid under the proposed semantics."


Темная сторона


И как бы, все эти соображения на виду. Но есть и другая, темная сторона вопроса. Существуют другие люди, более практически настроенные. У них есть собственный стандартный ответ: разработчики, упарывающиеся по Солиду, по принципу Лисков итп – это просто теоретики. В реальной жизни все не так, и на самом деле синглтоны нужны. И для этого у них есть специальная философия. Контр-философия.


Глобальное состояние есть везде


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


Как минимум, глобальная переменная – это наш реальный мир, в котором мы все живем. Если у нас какая-то компьютерная игра, MMORPG, мы ее пишем, то единый виртуальный мир игры – это тоже глобальное состояние. У нас не будет другой жизни и другого мира.


Более локально, если у вас в системе есть единственная база данных, и это часть предметной области, то зачастую имеет смысл привязаться к её единственности. Иначе на решение прикладной задачи не останется времени, и все начнут писать систему управления бесконечным количеством баз данных.


Аргумент про порядок тестов тут тоже не сработает: очевидно, что при исполнении, первой должна подниматься наша единственная база (и выполняться тесты относительно базы), и потом уже прикладной код. Если поднять код раньше базы, то все упадет. Большинство проектов, которые я видел, явным образом запускали базу до кода, и не видели в этом вообще никаких практических проблем.


"It just works" лучше явного


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


И рано или поздно, в вашем проекте оказываются тысячи классов, сотни и тысячи зависимостей на внешние библиотеки. Начиная с какого-то момента, ты не можешь управлять ими вручную, появляются умные системы сборки типа Maven. Maven появился, насколько помню, от ужаса, с которым встретились разработчики при сборке проектов на веб-фреймворке Turbine.


Рано или поздно ты понимаешь, что система становится умонепостигаемой. Ее сложность слишком велика, чтобы поместиться в мозг одному человеку, пусть это даже самый главный важный архитектор. И начинаешь искать такую автоматику, которая будет сама думать о внутренних связях, об индикации и сопровождении ошибок, об автоматическом dataflow, и так далее.


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


Жесткое лучше мягкого


Если проект большой, и в нем сложно разобраться, то прямые жесткие связи упрощают понимание кода. В случае ошибки, в стектрейсе будет именно то место, где она возникла. Открываются возможности по построению очень простых графиков, отражающих структуру кода. Сравните это с действительно слабой связанностью кода на основе очередей, когда вылетевшую в одном месте системы ошибку на самом деле нужно искать на другой стороне очереди, или нескольких очередей – такие поиски могут занимать часы.


Что касается тестирования. Если посмотреть, как это в реальности, то во многих проектах тестирование либо вообще не делают, либо делают в очень урезанном виде. В проекте может быть тысячи классов, но всего несколько десятков тестов. И ради этого крошечного количества вообще не имеет смысла беспокоиться, и переписывать систему особым образом. Это касается не только синглтонов, а вообще всего – настоящее полномасштабное тестирование тянет за собой очень много дополнительных условий. На практике ставить во главу угла тестируемость – не всегда хорошая идея.


Специфика Java


Да, написание синглтона выглядит некрасиво. Но в IDE всегда можно создать шаблон «некрасивого» класса, и навсегда забыть о том, чтобы писать этот неприятный код вручную. Как говорится, темнота – друг молодежи.


Решение


И вот, мы оказались на пороге реального конфликта: одни люди хотят синглтоны (на самом деле, просто глобальное состояние, просто они его называют синглтоном), а другие – наоборот сильно против этого.


Отличным решением является переход от настоящих синглтонов к сиглтонам курильщика… ой ой. Singleton Beans из Spring. В чем суть: с помощью аннотации Component и Scope(SCOPE_SINGLETON) вы помечаете некоторые классы как синглтоны.


import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class Greeter {
    public String hello() {
        return "Hello World!";
    }
}

Потом в любом месте вы можете сделать поле, пометить его как @Autowired, и при старте приложения в этом поле окажется нужный экземпляр.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloWorldController {
    @Autowired
    Greeter greeter;

    @RequestMapping(path = "/")
    public String home() {
        return greeter.hello();
    }
}

Заметьте, что этот вариант решает все перечисленные выше проблемы.


  • Оно позволяет использовать глобальное состояние, но при этом не запирает вас в рамках одного контекста. Вы в Спринге можете делать сколько угодно контекстов. Если необходимо сделать отдельный контекст для тестирования, это делается в несколько строк кода. Более того, оно предоставляет удобные инструменты для работы с контекстом, наспример, можно получить список всех существующих сейчас синглтонов. Как это сделать в чистой джаве – наверное, никак.
  • Оно позволяет явно описать все существующие сейчас синглтоны. Но не тратить время на ручное управление зависимостями. То есть, в коде присуствует большая магия, но эта магия полностью контролируется, если нужно.
  • Оно не нарушает S в SOLID, потому что жизненным циклом управляет Spring
  • Оно выглядит красиво и лаконично, так как сводится к нескольким аннотациям, и вообще не заставляет писать boilerplate код.

Резюме


Вопрос синглтонов давным-давно решен. Удвительно, что находятся люди, которые об этом еще не знают. Решение — это Spring и другие системы инверсии контроля и депенденси инжекшена. Синглтоны не нужно выкашивать, не нужно хетить людей, которые их пишут — их нужно переводить на Spring и обращать в нашу Spring-религию.


И еще


И раз уж вы не только посмотрели видос, но и дочитали до конца. Спасибо, за поглощение моего контента, оказали мне этим большую честь. (серьезно.) А теперь что с вас нужно: обязательно оставьте комментарий к этой статье, поставьте лайк, подпишитесь на хаб Java на Хабре, на блог друганов из JUG.ru которые замотивировали меня выложить этот горячечный брейндамп в сеть, и на других видных видеоблоггеров.


Пока!


Tags:
Hubs:
+39
Comments 79
Comments Comments 79

Articles