Паттерн Singleton
Добро пожаловать в руководство по использованию паттерна Singleton в языке программирования Dart. Этот паттерн проектирования является одним из самых распространённых и простых, но при этом невероятно полезным инструментом в арсенале разработчика.
Что такое Singleton?
Singleton (Одиночка) — это паттерн проектирования, который гарантирует наличие только одного экземпляра класса и предоставляет глобальную точку доступа к нему. Проще говоря: сколько бы раз вы ни пытались создать объект Singleton, вы всегда будете получать один и тот же экземпляр.
Зачем нужен Singleton?
Паттерн Singleton решает несколько распространённых задач:
- Экономия ресурсов: создание тяжёлых объектов, например, подключения к БД.
- Глобальная точка доступа: единый доступ к данным (например, к настройкам приложения).
- Координация состояния: синхронизация состояния между разными частями программы.
Реализация Singleton в Dart
В Dart существует несколько способов реализации паттерна Singleton. Разберем два наиболее популярных:
1. Через фабричный конструктор
class AppSettings {
// Приватный статический экземпляр (eager initialization)
static final AppSettings _instance = AppSettings._internal();
// Фабричный конструктор
factory AppSettings() {
return _instance;
}
// Приватный конструктор
AppSettings._internal();
// Настройки приложения
bool isDarkMode = false;
String language = 'ru';
double fontSize = 14.0;
void saveSettings() {
print('Настройки сохранены: тема = $isDarkMode, язык = $language, шрифт = $fontSize');
}
}
Особенности:
- Экземпляр создаётся сразу при загрузке класса (жадная инициализация).
- Полная потокобезопасность благодаря однопоточному исполнению в Dart.
2. Через ленивую инициализацию
class DataCache {
// Приватный статический экземпляр (lazy initialization)
static DataCache? _instance;
// Геттер для получения экземпляра
static DataCache get instance {
_instance ??= DataCache._internal();
return _instance!;
}
// Приватный конструктор
DataCache._internal();
// Кеш данных
final Map<String, dynamic> _cache = {};
Future<dynamic> getData(String key) async {
if (_cache.containsKey(key)) {
print('Получение из кеша: $key');
return _cache[key];
}
print('Загрузка данных: $key');
await Future.delayed(Duration(seconds: 2)); // эмуляция задержки
final data = 'Данные для $key (${DateTime.now()})';
_cache[key] = data;
return data;
}
void clearCache() {
_cache.clear();
print('Кеш очищен');
}
}
Особенности:
- Экземпляр создаётся только при первом обращении.
- Удобно, когда создание экземпляра "тяжёлое" или может не понадобиться сразу.
Быстрая таблица: eager vs lazy
Тип | Когда создаётся | Плюсы | Минусы |
---|---|---|---|
Eager | При старте программы | Простота, надёжность | Занимает память сразу |
Lazy | При первом использовании | Экономия ресурсов | Нужен осторожный код при многопоточности |
Когда использовать Singleton?
Применение Singleton оправдано в следующих случаях:
- Работа с подключением к базе данных.
- Менеджеры состояния и кэширования.
- Глобальные настройки приложения.
- Классы-обёртки для внешних сервисов.
Недостатки Singleton
Несмотря на удобство, Singleton имеет недостатки:
-
Глобальное состояние Усложняет понимание работы приложения и его тестирование.
-
Жёсткая связанность компонентов Модули напрямую зависят от конкретной реализации Singleton.
-
Трудности с модульным тестированием Подменить Singleton на мок-объект непросто без дополнительных абстракций.
-
Потенциальные утечки памяти Singleton "живет" всё время работы приложения — объекты внутри него никогда не удаляются.
-
Сложность масштабирования В многопоточной среде (например, при работе с isolates) Singleton нужно реализовывать осторожно.
Заключение
Паттерн Singleton — мощный инструмент в разработке приложений. Dart делает его реализацию особенно элегантной благодаря фабричным конструкторам и возможностям ленивой инициализации.
Главное правило: Использовать Singleton там, где это оправдано, а не по умолчанию!
Полезные ссылки
- Официальная документация Dart: Factory Constructors
- Статья о Singleton на Wikipedia
- Шаблоны проектирования на Dart (GitHub)
Домашнее задание
Базовый уровень:
Создайте класс Logger
с использованием Singleton.
Методы:
log(String message)
: пишет сообщение в консоль и сохраняет его в список истории.clear()
: очищает историю.history
: возвращает все сохранённые сообщения.
(Подсказка: используйте List
Средний уровень:
Реализуйте класс UserSession
для хранения информации о текущем пользователе.
Методы:
login(String username)
logout()
isLoggedIn()
(Подсказка: сохраняйте имя пользователя в поле класса.)
Продвинутый уровень:
Создайте менеджер загрузки изображений ImageLoader
.
Методы:
load(String url)
: загружает изображение (если уже есть в кеше — возвращает кешированное).clearCache()
cacheStats()
: количество кешированных изображений.
(Подсказка: используйте Map<String, dynamic> для кеша.)
Экспертный уровень:
Реализуйте DatabaseConnectionManager
с поддержкой двух типов подключений:
- основная база (
primary
) - база только для чтения (
readReplica
)
Используйте абстрактную фабрику для создания нужного подключения.
(Подсказка: определите enum ConnectionType
.)