Итак, синглтоны плохи, тогда что?

Итак, синглтоны плохи, тогда что?
Итак, синглтоны плохи, тогда что? - matteo_skyrider @ Unsplash

В последнее время было много дискуссий о проблемах с использованием (и чрезмерным использованием) синглтонов. Я тоже был одним из таких людей в начале своей карьеры. Теперь я понимаю, в чем проблема, и тем не менее во многих случаях я не вижу хорошей альтернативы, и не многие из дискуссий против Синглтона действительно предлагают ее.

Вот реальный пример из крупного недавнего проекта, в котором я участвовал:

Приложение представляло собой толстый клиент с множеством отдельных экранов и компонентов, который использует огромные объемы данных из состояния сервера, которое не обновляется слишком часто. Эти данные были в основном кэшированы в объекте «менеджера» Singleton — ужасном «глобальном состоянии». Идея заключалась в том, чтобы иметь одно место в приложении, в котором хранятся и синхронизируются данные, а затем любые открываемые новые экраны могут просто запрашивать большую часть того, что им нужно, без повторных запросов различных вспомогательных данных с сервера. Постоянные запросы к серверу потребовали бы слишком большой пропускной способности — и я говорю о дополнительных счетах за Интернет на тысячи долларов в неделю, так что это было неприемлемо.

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

Здесь важно различать отдельные экземпляры и шаблон проектирования Singleton.

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

Шаблон проектирования Singleton — это очень специфический тип одиночного экземпляра, а именно:

  • Доступен через глобальное статическое поле экземпляра;
  • Создается либо при инициализации программы, либо при первом доступе;
  • Нет общедоступного конструктора (не может создавать экземпляры напрямую);
  • Никогда не освобождается явно (неявно освобождается при завершении программы).

Именно из-за этого конкретного выбора дизайна шаблон представляет несколько потенциальных долгосрочных проблем:

  • Невозможность использования абстрактных или интерфейсных классов;
  • Невозможность подкласса;
  • Высокая связь между приложением (сложно модифицировать);
  • Сложно тестировать (нельзя подделывать/мокать в юнит-тестах);
  • Трудно распараллелить в случае изменяемого состояния (требуется обширная блокировка);
  • и так далее.

Ни один из этих симптомов на самом деле не является эндемичным для отдельных экземпляров, только шаблон Singleton.

Что вы можете сделать вместо этого? Просто не используйте шаблон Singleton.

Цитата из вопроса:

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

У этой концепции есть имя, как вы намекаете, но звучите неуверенно. Это называется кеш. Если вы хотите пофантазировать, вы можете назвать это «автономным кешем» или просто автономной копией удаленных данных.

Кэш не обязательно должен быть одноэлементным. Это может быть один экземпляр, если вы хотите избежать выборки одних и тех же данных для нескольких экземпляров кэша; но это не значит, что вы действительно должны выставлять все напоказ всем.

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

                          MSAccessCache
                                ▲
                                |
              +-----------------+-----------------+
              |                 |                 |
         IMediaCache      IProfileCache      IPageCache
              |                 |                 |
              |                 |                 |
          VideoPage       MyAccountPage     MostPopularPage

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

Кстати, это называется Dependency Injection; вам не нужно использовать Spring или какой-либо специальный контейнер IoC, пока ваш общий дизайн класса принимает его зависимости от вызывающего объекта вместо того, чтобы создавать их экземпляры самостоятельно или ссылаться на глобальное состояние.

Почему вы должны использовать дизайн, основанный на интерфейсе? Три причины:

  1. Это облегчает чтение кода; вы можете четко понять из интерфейсов, от каких именно данных зависят зависимые классы.

  2. Если и когда вы поймете, что Microsoft Access не был лучшим выбором для серверной части данных, вы можете заменить его чем-то лучшим, скажем, SQL Server.

  3. Если и когда вы поймете, что SQL Server не является лучшим выбором конкретно для носителей, вы можете разбить свою реализацию, не затрагивая никакие другие части системы. Вот где проявляется настоящая сила абстракции.

Если вы хотите сделать еще один шаг вперед, вы можете использовать контейнер IoC (инфраструктуру DI), например Spring (Java) или Unity (.NET). Почти каждый DI-фреймворк выполняет собственное управление жизненным циклом и, в частности, позволяет определить конкретный сервис как отдельный экземпляр (часто называя его «одиночным», но это только для ознакомления). По сути, эти фреймворки избавляют вас от большей части обезьяньей работы по ручной передаче экземпляров, но они не являются строго необходимыми. Вам не потребуются никакие специальные инструменты для того, чтобы реализовать этот дизайн.

Для полноты картины я должен отметить, что приведенный выше дизайн также не идеален. Когда вы имеете дело с кешем (как и вы), у вас должен быть совершенно отдельный слой. Другими словами, такой дизайн:

                                                        +--IMediaRepository
                                                        |
                          Cache (Generic)---------------+--IProfileRepository
                                ▲                       |
                                |                       +--IPageRepository
              +-----------------+-----------------+
              |                 |                 |
         IMediaCache      IProfileCache      IPageCache
              |                 |                 |
              |                 |                 |
          VideoPage       MyAccountPage     MostPopularPage

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

Ничто из этого не означает, что каждая отдельная часть программного обеспечения в мире должна быть спроектирована в соответствии с этими строгими стандартами высокой согласованности и слабой связанности; это зависит от размера и масштаба проекта, вашей команды, вашего бюджета, сроков и т. д. Но если вы спрашиваете, какой дизайн лучше (использовать вместо синглтона), то это он.

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


LetsCodeIt, 9 июня 2023 г., 16:47