0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><a>#статьи</a></p>
1
<p><a>#статьи</a></p>
2
<ul><li>5 окт 2023</li>
2
<ul><li>5 окт 2023</li>
3
<li>0</li>
3
<li>0</li>
4
</ul><p>Знакомимся с одним из самых популярных и спорных паттернов проектирования, его реализациями и альтернативой.</p>
4
</ul><p>Знакомимся с одним из самых популярных и спорных паттернов проектирования, его реализациями и альтернативой.</p>
5
<p>Иллюстрация: Катя Павловская для Skillbox Media</p>
5
<p>Иллюстрация: Катя Павловская для Skillbox Media</p>
6
<p>Пишет о сетях, инструментах для разработчиков и языках программирования. Любит готовить, играть в инди‑игры и программировать на Python.</p>
6
<p>Пишет о сетях, инструментах для разработчиков и языках программирования. Любит готовить, играть в инди‑игры и программировать на Python.</p>
7
<p>Паттерны проектирования - это лучшие практики написания кода, которые помогают разработчикам не изобретать велосипеды, а писать программы по проверенным рецептам. Многие из них впервые были описаны в книге "<a>Паттерны объектно-ориентированного проектирования</a>" Эриха Гамма, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса.</p>
7
<p>Паттерны проектирования - это лучшие практики написания кода, которые помогают разработчикам не изобретать велосипеды, а писать программы по проверенным рецептам. Многие из них впервые были описаны в книге "<a>Паттерны объектно-ориентированного проектирования</a>" Эриха Гамма, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса.</p>
8
<p>Среди этих паттернов есть свой Гамлет - такой же противоречивый и непредсказуемый. Речь о Singleton, шаблоне проектирования "одиночка". Сегодня вы узнаете:</p>
8
<p>Среди этих паттернов есть свой Гамлет - такой же противоречивый и непредсказуемый. Речь о Singleton, шаблоне проектирования "одиночка". Сегодня вы узнаете:</p>
9
<ul><li><a>Что это такое</a></li>
9
<ul><li><a>Что это такое</a></li>
10
<li><a>Где применяется</a></li>
10
<li><a>Где применяется</a></li>
11
<li><a>Как он работает</a></li>
11
<li><a>Как он работает</a></li>
12
<li><a>Какие проблемы в работе с ним могут возникнуть</a></li>
12
<li><a>Какие проблемы в работе с ним могут возникнуть</a></li>
13
<li><a>Как реализовать шаблон</a></li>
13
<li><a>Как реализовать шаблон</a></li>
14
<li><a>Что выбрать в проекте: Singleton или статический класс</a></li>
14
<li><a>Что выбрать в проекте: Singleton или статический класс</a></li>
15
</ul><p>Singleton (с англ. "одиночка") - это паттерн проектирования, гарантирующий, что у класса будет только один экземпляр. К этому экземпляру будет предоставлена глобальная, то есть доступная из любой части программы, точка доступа. Если попытаться создать новый объект этого класса, то вернётся уже созданный существующий экземпляр.</p>
15
</ul><p>Singleton (с англ. "одиночка") - это паттерн проектирования, гарантирующий, что у класса будет только один экземпляр. К этому экземпляру будет предоставлена глобальная, то есть доступная из любой части программы, точка доступа. Если попытаться создать новый объект этого класса, то вернётся уже созданный существующий экземпляр.</p>
16
<p>Например, в любом государстве может быть только одна конституция. Если потребуется её изменить, то нужно будет работать с существующим экземпляром и никак иначе.</p>
16
<p>Например, в любом государстве может быть только одна конституция. Если потребуется её изменить, то нужно будет работать с существующим экземпляром и никак иначе.</p>
17
<p>Другой пример - менеджер паролей. Он создаёт только один объект для каждой учётной записи и возвращает его при запросе - для одной учётной записи не может быть двух разных паролей.</p>
17
<p>Другой пример - менеджер паролей. Он создаёт только один объект для каждой учётной записи и возвращает его при запросе - для одной учётной записи не может быть двух разных паролей.</p>
18
<p><strong>Конфигурационные настройки.</strong>Представим, что у нас есть класс с настройками приложения - параметрами базы данных или внешнего вида интерфейса. Имеет смысл реализовать его как Singleton. Это обеспечит одну точку доступа к настройкам, и весь код сможет ссылаться на одни и те же настройки.</p>
18
<p><strong>Конфигурационные настройки.</strong>Представим, что у нас есть класс с настройками приложения - параметрами базы данных или внешнего вида интерфейса. Имеет смысл реализовать его как Singleton. Это обеспечит одну точку доступа к настройкам, и весь код сможет ссылаться на одни и те же настройки.</p>
19
<p><strong>Подключение к базе данных.</strong>Если наше приложение использует базу данных, Singleton гарантированно создаст только один экземпляр класса, отвечающего за подключение к ней. Так мы предотвратим лишние соединения и упростим подключение в целом.</p>
19
<p><strong>Подключение к базе данных.</strong>Если наше приложение использует базу данных, Singleton гарантированно создаст только один экземпляр класса, отвечающего за подключение к ней. Так мы предотвратим лишние соединения и упростим подключение в целом.</p>
20
<p><strong>Логирование.</strong>Singleton удобно использовать для логов. Вместо создания нового логгера каждый раз, когда нужно что-то залогировать, мы записываем всё в один объект.</p>
20
<p><strong>Логирование.</strong>Singleton удобно использовать для логов. Вместо создания нового логгера каждый раз, когда нужно что-то залогировать, мы записываем всё в один объект.</p>
21
<p><strong>Счётчики и глобальные объекты.</strong>Паттерн "одиночка" подходит для оценки состояния приложения или сбора статистики с его модулей. С классом можно будет взаимодействовать из любой части программы.</p>
21
<p><strong>Счётчики и глобальные объекты.</strong>Паттерн "одиночка" подходит для оценки состояния приложения или сбора статистики с его модулей. С классом можно будет взаимодействовать из любой части программы.</p>
22
<p><strong>Пул ресурсов.</strong>Если у нас ограниченный пул соединений к внешнему сервису или к другим ресурсам, то Singleton гарантирует, что доступ к ним всегда будет идти через единственный экземпляр.</p>
22
<p><strong>Пул ресурсов.</strong>Если у нас ограниченный пул соединений к внешнему сервису или к другим ресурсам, то Singleton гарантирует, что доступ к ним всегда будет идти через единственный экземпляр.</p>
23
<p>Классическая реализация шаблона "одиночка" - конструктор и метод getInstance(). В коде на языках программирования, использующих объектно-ориентированный подход, паттерн выглядит примерно одинаково:</p>
23
<p>Классическая реализация шаблона "одиночка" - конструктор и метод getInstance(). В коде на языках программирования, использующих объектно-ориентированный подход, паттерн выглядит примерно одинаково:</p>
24
<p><strong>Python</strong></p>
24
<p><strong>Python</strong></p>
25
class Singleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(Singleton, cls).__new__(cls) return cls._instance def __init__(self): # Этот конструктор вызывается только при первом создании экземпляра pass # Использование singleton1 = Singleton() singleton2 = Singleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p><strong>Java</strong></p>
25
class Singleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(Singleton, cls).__new__(cls) return cls._instance def __init__(self): # Этот конструктор вызывается только при первом создании экземпляра pass # Использование singleton1 = Singleton() singleton2 = Singleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p><strong>Java</strong></p>
26
public class Singleton { private static Singleton instance; private Singleton() { // Приватный конструктор } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } // Использование Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = Singleton.getInstance(); System.out.println(singleton1 == singleton2); // Выведет: True, так как это один и тот же экземпляр<p>Пока что код в примерах выглядит сложно, но мы разберём его дальше. Чтобы было понятнее, рассмотрим базовый шаблон Singleton:</p>
26
public class Singleton { private static Singleton instance; private Singleton() { // Приватный конструктор } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } // Использование Singleton singleton1 = Singleton.getInstance(); Singleton singleton2 = Singleton.getInstance(); System.out.println(singleton1 == singleton2); // Выведет: True, так как это один и тот же экземпляр<p>Пока что код в примерах выглядит сложно, но мы разберём его дальше. Чтобы было понятнее, рассмотрим базовый шаблон Singleton:</p>
27
class Singleton: _instance = None # Приватное поле для хранения единственного экземпляра класса. Пока здесь None, то есть ничего def __new__(cls): if cls._instance is None: # Если экземпляр ещё не создан cls._instance = super(Singleton, cls).__new__(cls) # Создаём экземпляр с помощью приватного конструктора __new__ return cls._instance # Возвращаем или созданный ранее, или существующий экземпляр<p>Здесь всё просто. Создаём экземпляр в том случае, если он ещё не создан. Приватный конструктор __new__ гарантирует, что экземпляр класса можно создать только внутри самого класса. Возвращаем либо созданный ранее экземпляр, либо тот, что создали только что.</p>
27
class Singleton: _instance = None # Приватное поле для хранения единственного экземпляра класса. Пока здесь None, то есть ничего def __new__(cls): if cls._instance is None: # Если экземпляр ещё не создан cls._instance = super(Singleton, cls).__new__(cls) # Создаём экземпляр с помощью приватного конструктора __new__ return cls._instance # Возвращаем или созданный ранее, или существующий экземпляр<p>Здесь всё просто. Создаём экземпляр в том случае, если он ещё не создан. Приватный конструктор __new__ гарантирует, что экземпляр класса можно создать только внутри самого класса. Возвращаем либо созданный ранее экземпляр, либо тот, что создали только что.</p>
28
<p>Дальше мы будем употреблять понятие "поток", "многопоточность", "потокобезопасность" и другие производные от "потока". В программировании потоком называют независимую последовательность инструкций.</p>
28
<p>Дальше мы будем употреблять понятие "поток", "многопоточность", "потокобезопасность" и другие производные от "потока". В программировании потоком называют независимую последовательность инструкций.</p>
29
<p>Разберём эти понятия на простом примере. Представьте, что вам необходимо приготовить какое-то блюдо, например пирожки с картофелем. Чтобы получить конечный результат - готовый пирожок, нужно приготовить тесто и начинку. Быстрее будет разделить задачу на двоих - один замешивает тесто, а второй - толчёт картофель, добавляет туда соль, лук и так далее. Так мы реализуем<strong>многопоточность</strong>,<strong></strong>то есть одновременное выполнение нескольких задач.</p>
29
<p>Разберём эти понятия на простом примере. Представьте, что вам необходимо приготовить какое-то блюдо, например пирожки с картофелем. Чтобы получить конечный результат - готовый пирожок, нужно приготовить тесто и начинку. Быстрее будет разделить задачу на двоих - один замешивает тесто, а второй - толчёт картофель, добавляет туда соль, лук и так далее. Так мы реализуем<strong>многопоточность</strong>,<strong></strong>то есть одновременное выполнение нескольких задач.</p>
30
<p>Если инструкции перепутаются, то в пюре попадёт мука, а тесто засорит толкушку - пирожков не получится. Поэтому нам нужно реализовать<strong>потокобезопасность</strong> - не допустить попадания инструкций в не предназначенное для них место. Это важно и для программ - если инструкция из одного потока попадёт в другой, на выходе мы получим неправильный результат или программа вовсе не скомпилируется.</p>
30
<p>Если инструкции перепутаются, то в пюре попадёт мука, а тесто засорит толкушку - пирожков не получится. Поэтому нам нужно реализовать<strong>потокобезопасность</strong> - не допустить попадания инструкций в не предназначенное для них место. Это важно и для программ - если инструкция из одного потока попадёт в другой, на выходе мы получим неправильный результат или программа вовсе не скомпилируется.</p>
31
<p>Паттерн ругают за то, что он доставляет больше проблем, чем решает. Такая критика иногда обоснованна. Вот основные проблемы при его использовании:</p>
31
<p>Паттерн ругают за то, что он доставляет больше проблем, чем решает. Такая критика иногда обоснованна. Вот основные проблемы при его использовании:</p>
32
<p><strong>Глобальное состояние.</strong>Singleton доступен из любой части программы, то есть сломать его может кто угодно. Разберём на примере.</p>
32
<p><strong>Глобальное состояние.</strong>Singleton доступен из любой части программы, то есть сломать его может кто угодно. Разберём на примере.</p>
33
<p>Синглтон GameManager управляет состоянием игры:</p>
33
<p>Синглтон GameManager управляет состоянием игры:</p>
34
class GameManager: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(GameManager, cls).__new__(cls) cls._instance.state = "initialized" return cls._instance # Использование game_manager1 = GameManager() game_manager2 = GameManager() game_manager1.state = "running" print(game_manager2.state) # Выведет: "running", так как state - общее для всех экземпляров<p>Здесь мы собрали синглтон по шаблону, и добавили ему описание состояния cls._instance.state = "initialized", которое потом поменяли на "running" в одной переменной. Но так как переменные ссылаются на один и тот же объект, изменения коснутся их всех.</p>
34
class GameManager: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(GameManager, cls).__new__(cls) cls._instance.state = "initialized" return cls._instance # Использование game_manager1 = GameManager() game_manager2 = GameManager() game_manager1.state = "running" print(game_manager2.state) # Выведет: "running", так как state - общее для всех экземпляров<p>Здесь мы собрали синглтон по шаблону, и добавили ему описание состояния cls._instance.state = "initialized", которое потом поменяли на "running" в одной переменной. Но так как переменные ссылаются на один и тот же объект, изменения коснутся их всех.</p>
35
<p><strong>Потокобезопасность.</strong>Базовая реализация паттерна "одиночка" непотокобезопасна. Если не предусмотрены механизмы синхронизации, разные потоки могут наштамповать кучу синглтонов.</p>
35
<p><strong>Потокобезопасность.</strong>Базовая реализация паттерна "одиночка" непотокобезопасна. Если не предусмотрены механизмы синхронизации, разные потоки могут наштамповать кучу синглтонов.</p>
36
import threading class ThreadUnsafeSingleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(ThreadUnsafeSingleton, cls).__new__(cls) return cls._instance def worker(): singleton = ThreadUnsafeSingleton() print(singleton) # Запуск нескольких потоков threads = [] for _ in range(5): thread = threading.Thread(target=worker) threads.append(thread) thread.start() # Вывод: может быть несколько разных экземпляров ThreadUnsafeSingleton<p>Здесь в одном из потоков условие cls._instance is None выполняется, и экземпляр создаётся. Но до того, как он будет присвоен переменной _instance, другие потоки могут также пройти через это условие и создать свои экземпляры. И наш Singleton уже будет не single.</p>
36
import threading class ThreadUnsafeSingleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(ThreadUnsafeSingleton, cls).__new__(cls) return cls._instance def worker(): singleton = ThreadUnsafeSingleton() print(singleton) # Запуск нескольких потоков threads = [] for _ in range(5): thread = threading.Thread(target=worker) threads.append(thread) thread.start() # Вывод: может быть несколько разных экземпляров ThreadUnsafeSingleton<p>Здесь в одном из потоков условие cls._instance is None выполняется, и экземпляр создаётся. Но до того, как он будет присвоен переменной _instance, другие потоки могут также пройти через это условие и создать свои экземпляры. И наш Singleton уже будет не single.</p>
37
<p><strong>Тестирование.</strong>Для тестирования класса с Singleton потребуются мок-объекты. А заменить реальный объект на мок не всегда легко или вообще возможно.</p>
37
<p><strong>Тестирование.</strong>Для тестирования класса с Singleton потребуются мок-объекты. А заменить реальный объект на мок не всегда легко или вообще возможно.</p>
38
<p>Рассмотрим DatabaseConnection - синглтон, который устанавливает и поддерживает соединение с базой данных. Тестировать код, зависящий от него, сложно - в тестах придётся подменить все соединения на мок-объекты:</p>
38
<p>Рассмотрим DatabaseConnection - синглтон, который устанавливает и поддерживает соединение с базой данных. Тестировать код, зависящий от него, сложно - в тестах придётся подменить все соединения на мок-объекты:</p>
39
class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(DatabaseConnection, cls).__new__(cls) cls._instance.connect() return cls._instance def connect(self): print("Connected to the database") class UserDatabaseService: def __init__(self): self.db_connection = DatabaseConnection() def create_user(self, username): # Создание пользователя в базе данных print(f"User '{username}' created") # Теперь представим, что мы хотим протестировать UserDatabaseService user_service = UserDatabaseService() # Проблема: мы не можем легко подменить DatabaseConnection на мок-объект для тестов<p>Здесь мы создали экземпляр класса UserDatabaseService через конструктор класса DatabaseConnection, который является синглтоном. Чтобы покрыть тестами UserDatabaseService, нам придётся лезть в DatabaseConnection и вручную подгонять его под тестовые условия.</p>
39
class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(DatabaseConnection, cls).__new__(cls) cls._instance.connect() return cls._instance def connect(self): print("Connected to the database") class UserDatabaseService: def __init__(self): self.db_connection = DatabaseConnection() def create_user(self, username): # Создание пользователя в базе данных print(f"User '{username}' created") # Теперь представим, что мы хотим протестировать UserDatabaseService user_service = UserDatabaseService() # Проблема: мы не можем легко подменить DatabaseConnection на мок-объект для тестов<p>Здесь мы создали экземпляр класса UserDatabaseService через конструктор класса DatabaseConnection, который является синглтоном. Чтобы покрыть тестами UserDatabaseService, нам придётся лезть в DatabaseConnection и вручную подгонять его под тестовые условия.</p>
40
<p><strong>Нарушение принципа единственной ответственности.</strong>Принципы грамотного проектирования и просто хороший тон гласят: каждый класс должен делать только одну важную вещь. Другими словами, класс должен быть специализированным.</p>
40
<p><strong>Нарушение принципа единственной ответственности.</strong>Принципы грамотного проектирования и просто хороший тон гласят: каждый класс должен делать только одну важную вещь. Другими словами, класс должен быть специализированным.</p>
41
<p>Если мы нарушаем этот принцип, то один класс получит слишком много разных обязанностей. Например, в одном классе есть код, который отвечает и за управление пользователями, и за запись логов в файл, и за работу с базой данных. Это хрестоматийный пример "спагетти-кода". Разобрать его сложно, а уж поддерживать - и подавно.</p>
41
<p>Если мы нарушаем этот принцип, то один класс получит слишком много разных обязанностей. Например, в одном классе есть код, который отвечает и за управление пользователями, и за запись логов в файл, и за работу с базой данных. Это хрестоматийный пример "спагетти-кода". Разобрать его сложно, а уж поддерживать - и подавно.</p>
42
<p>Вот пример того, как не надо прописывать класс:</p>
42
<p>Вот пример того, как не надо прописывать класс:</p>
43
class Logger: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(Logger, cls).__new__(cls) return cls._instance def log(self, message): print("Logging:", message) class UserManager: def __init__(self): self.logger = Logger() # Зависимость от Singleton def create_user(self, username): self.logger.log(f"User '{username}' created") user_manager = UserManager() user_manager.create_user("john_doe")<p>Здесь UserManager отвечает за создание пользователей и в то же время создаёт и использует логгер. Это усложняет код. Лучше разделить эти две функции между отдельными классами:</p>
43
class Logger: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(Logger, cls).__new__(cls) return cls._instance def log(self, message): print("Logging:", message) class UserManager: def __init__(self): self.logger = Logger() # Зависимость от Singleton def create_user(self, username): self.logger.log(f"User '{username}' created") user_manager = UserManager() user_manager.create_user("john_doe")<p>Здесь UserManager отвечает за создание пользователей и в то же время создаёт и использует логгер. Это усложняет код. Лучше разделить эти две функции между отдельными классами:</p>
44
class Logger: def log(self, message): print("Logging:", message) class UserManager: def __init__(self): self.logger = Logger() def create_user(self, username): self.logger.log(f"User '{username}' created") class Database: def save_user(self, user_data): print("Saving user to the database:", user_data) class UserService: def __init__(self): self.user_manager = UserManager() self.database = Database() def create_user(self, username): self.user_manager.create_user(username) user_data = {"username": username} self.database.save_user(user_data) # Использование user_service = UserService() user_service.create_user("john_doe")<p>В этом примере мы разделили функции UserManager на два отдельных класса: UserManager и Database. UserManager теперь отвечает только за создание пользователей и использование логгера, а Database занимается сохранением данных о пользователях в базу данных.</p>
44
class Logger: def log(self, message): print("Logging:", message) class UserManager: def __init__(self): self.logger = Logger() def create_user(self, username): self.logger.log(f"User '{username}' created") class Database: def save_user(self, user_data): print("Saving user to the database:", user_data) class UserService: def __init__(self): self.user_manager = UserManager() self.database = Database() def create_user(self, username): self.user_manager.create_user(username) user_data = {"username": username} self.database.save_user(user_data) # Использование user_service = UserService() user_service.create_user("john_doe")<p>В этом примере мы разделили функции UserManager на два отдельных класса: UserManager и Database. UserManager теперь отвечает только за создание пользователей и использование логгера, а Database занимается сохранением данных о пользователях в базу данных.</p>
45
<p>Затем мы создали новый класс UserService, который объединяет функциональность UserManager и Database для удобства использования. Теперь каждый класс имеет свою чёткую ответственность, что делает код более понятным и удобным для изменения.</p>
45
<p>Затем мы создали новый класс UserService, который объединяет функциональность UserManager и Database для удобства использования. Теперь каждый класс имеет свою чёткую ответственность, что делает код более понятным и удобным для изменения.</p>
46
<p>Некоторые из этих проблем связаны с самой логикой паттерна "одиночка", но многие решаются его правильной реализацией.</p>
46
<p>Некоторые из этих проблем связаны с самой логикой паттерна "одиночка", но многие решаются его правильной реализацией.</p>
47
<p>Мы должны прописывать синглтон так, чтобы он был потокобезопасным, ленивым и многопоточным. Разберёмся в каждом термине и посмотрим на примеры кода.</p>
47
<p>Мы должны прописывать синглтон так, чтобы он был потокобезопасным, ленивым и многопоточным. Разберёмся в каждом термине и посмотрим на примеры кода.</p>
48
<p>Потокобезопасный Singleton гарантирует, что только один поток создаёт его экземпляр. Нам всего-то нужно прописать мьютексы - сделать так, чтобы доступ к важным ресурсам в определённый момент времени имел только один поток.</p>
48
<p>Потокобезопасный Singleton гарантирует, что только один поток создаёт его экземпляр. Нам всего-то нужно прописать мьютексы - сделать так, чтобы доступ к важным ресурсам в определённый момент времени имел только один поток.</p>
49
import threading class ThreadSafeSingleton: _instance = None _lock = threading.Lock() # Мьютекс для синхронизации def __new__(cls): with cls._lock: if cls._instance is None: cls._instance = super(ThreadSafeSingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = ThreadSafeSingleton() singleton2 = ThreadSafeSingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Здесь мы использовали функцию threading.Lock(), чтобы показать другим потокам, что они пока не могут создавать собственные синглтоны.</p>
49
import threading class ThreadSafeSingleton: _instance = None _lock = threading.Lock() # Мьютекс для синхронизации def __new__(cls): with cls._lock: if cls._instance is None: cls._instance = super(ThreadSafeSingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = ThreadSafeSingleton() singleton2 = ThreadSafeSingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Здесь мы использовали функцию threading.Lock(), чтобы показать другим потокам, что они пока не могут создавать собственные синглтоны.</p>
50
<p>Ленивый Singleton создаёт экземпляр только при первом обращении к нему. Это позволяет отложить его создание до тех пор, пока он действительно не потребуется.</p>
50
<p>Ленивый Singleton создаёт экземпляр только при первом обращении к нему. Это позволяет отложить его создание до тех пор, пока он действительно не потребуется.</p>
51
class LazySingleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(LazySingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = LazySingleton() singleton2 = LazySingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Эта реализация очень напоминает наш шаблон. Всё потому, что создать неленивый синглтон на Python невозможно - этот язык не поддерживает создание экземпляров класса без обращения к нему, то есть синглтоны здесь ленивые по умолчанию. Поэтому посмотрим на Java:</p>
51
class LazySingleton: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(LazySingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = LazySingleton() singleton2 = LazySingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Эта реализация очень напоминает наш шаблон. Всё потому, что создать неленивый синглтон на Python невозможно - этот язык не поддерживает создание экземпляров класса без обращения к нему, то есть синглтоны здесь ленивые по умолчанию. Поэтому посмотрим на Java:</p>
52
public class NonLazySingleton { private static final NonLazySingleton instance = new NonLazySingleton(); // Приватный конструктор, чтобы предотвратить создание экземпляров извне private NonLazySingleton() {} // Метод для получения единственного экземпляра public static NonLazySingleton getInstance() { return instance; } }<p>Здесь мы прописали в статическом поле класса ключевое слово final, и тем самым сделали переменную instance константой и там же создали экземпляр. А так как в Java статические поля исполняются при загрузке, обратимся мы к классу или нет - неважно, наш синглтон уже создан.</p>
52
public class NonLazySingleton { private static final NonLazySingleton instance = new NonLazySingleton(); // Приватный конструктор, чтобы предотвратить создание экземпляров извне private NonLazySingleton() {} // Метод для получения единственного экземпляра public static NonLazySingleton getInstance() { return instance; } }<p>Здесь мы прописали в статическом поле класса ключевое слово final, и тем самым сделали переменную instance константой и там же создали экземпляр. А так как в Java статические поля исполняются при загрузке, обратимся мы к классу или нет - неважно, наш синглтон уже создан.</p>
53
<p>Многопоточный Singleton обеспечивает безопасное создание экземпляра в многопоточной среде. Этот вариант использует двойную проверку для оптимизации создания:</p>
53
<p>Многопоточный Singleton обеспечивает безопасное создание экземпляра в многопоточной среде. Этот вариант использует двойную проверку для оптимизации создания:</p>
54
import threading class MultithreadedSingleton: _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super(MultithreadedSingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = MultithreadedSingleton() singleton2 = MultithreadedSingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Здесь уже знакомый нам threading.Lock() гарантирует, что в каждый определённый момент времени у нас будет только один экземпляр MultithreadedSingleton. А благодаря особенностям Python наш синглтон ещё и ленивый.</p>
54
import threading class MultithreadedSingleton: _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super(MultithreadedSingleton, cls).__new__(cls) return cls._instance # Использование singleton1 = MultithreadedSingleton() singleton2 = MultithreadedSingleton() print(singleton1 is singleton2) # Выведет: True, так как это один и тот же экземпляр<p>Здесь уже знакомый нам threading.Lock() гарантирует, что в каждый определённый момент времени у нас будет только один экземпляр MultithreadedSingleton. А благодаря особенностям Python наш синглтон ещё и ленивый.</p>
55
<p>Последняя реализация паттерна отлично подходит для решения задач. Но минусы есть и у неё:</p>
55
<p>Последняя реализация паттерна отлично подходит для решения задач. Но минусы есть и у неё:</p>
56
<p><strong>Костыли.</strong>В реализации синглтона мы прямо описываем механизм ленивой инициализации и блокировки. Класс становится сложнее для понимания и для отладки по сравнению с предыдущими вариантами.</p>
56
<p><strong>Костыли.</strong>В реализации синглтона мы прямо описываем механизм ленивой инициализации и блокировки. Класс становится сложнее для понимания и для отладки по сравнению с предыдущими вариантами.</p>
57
<p><strong>Скрытые зависимости.</strong>Наш синглтон зависит от правильной работы threading.Lock(). Если это неявное требование не будет чётко задокументировано, другие разработчики могут проигнорировать его и столкнуться с непредвиденными проблемами.</p>
57
<p><strong>Скрытые зависимости.</strong>Наш синглтон зависит от правильной работы threading.Lock(). Если это неявное требование не будет чётко задокументировано, другие разработчики могут проигнорировать его и столкнуться с непредвиденными проблемами.</p>
58
<p><strong>Избыточные блокировки.</strong>У нас блокировки используются только для инициализации. Но даже так они грозят оверхедом при одновременном доступе из разных потоков, что снижает скорость выполнения кода.</p>
58
<p><strong>Избыточные блокировки.</strong>У нас блокировки используются только для инициализации. Но даже так они грозят оверхедом при одновременном доступе из разных потоков, что снижает скорость выполнения кода.</p>
59
<p><strong>Антипаттерн "подделки".</strong>Синглтон реализуется через наследование от super().__new__(cls), а множественное наследование может вызвать неожиданные результаты. Если кто-нибудь пропишет ваш синглтон в качестве родительского класса, то сможет использовать его методы, но может запутаться, если у другого родителя будут одноимённые методы.</p>
59
<p><strong>Антипаттерн "подделки".</strong>Синглтон реализуется через наследование от super().__new__(cls), а множественное наследование может вызвать неожиданные результаты. Если кто-нибудь пропишет ваш синглтон в качестве родительского класса, то сможет использовать его методы, но может запутаться, если у другого родителя будут одноимённые методы.</p>
60
<p>Этих проблем можно избежать, если вместо синглтона использовать статический класс.</p>
60
<p>Этих проблем можно избежать, если вместо синглтона использовать статический класс.</p>
61
<p>Сразу оговоримся: далее мы будем использовать широкое понятие "статического класса", без привязки к конкретному языку программирования. Статическим будем называть класс, для которого нельзя создать новый экземпляр напрямую.</p>
61
<p>Сразу оговоримся: далее мы будем использовать широкое понятие "статического класса", без привязки к конкретному языку программирования. Статическим будем называть класс, для которого нельзя создать новый экземпляр напрямую.</p>
62
<p><strong>Простота.</strong>Статический класс более понятен и прозрачен, когда дело касается создания экземпляра. Методы и переменные используются напрямую, без getInstance().</p>
62
<p><strong>Простота.</strong>Статический класс более понятен и прозрачен, когда дело касается создания экземпляра. Методы и переменные используются напрямую, без getInstance().</p>
63
<p><strong>Оптимален для констант.</strong>Статический класс не требует создания экземпляров и предоставляет удобный доступ к значениям через статические поля. Если нужно хранить только константы или статические методы, то проще делать это в статическом методе.</p>
63
<p><strong>Оптимален для констант.</strong>Статический класс не требует создания экземпляров и предоставляет удобный доступ к значениям через статические поля. Если нужно хранить только константы или статические методы, то проще делать это в статическом методе.</p>
64
<p>Вот наглядный пример того, как статический класс упрощает код:</p>
64
<p>Вот наглядный пример того, как статический класс упрощает код:</p>
65
public class AppSettings { public static String API_KEY = "default_key"; public static String BASE_URL = "https://api.example.com"; } // Использование System.out.println(AppSettings.API_KEY); // Выведет: default_key System.out.println(AppSettings.BASE_URL); // Выведет: https://api.example.com<p>Если нужно вытащить значение из переменной класса, мы просто обращаемся к полю класса, без создания экземпляров.</p>
65
public class AppSettings { public static String API_KEY = "default_key"; public static String BASE_URL = "https://api.example.com"; } // Использование System.out.println(AppSettings.API_KEY); // Выведет: default_key System.out.println(AppSettings.BASE_URL); // Выведет: https://api.example.com<p>Если нужно вытащить значение из переменной класса, мы просто обращаемся к полю класса, без создания экземпляров.</p>
66
<p>А вот как выглядит код с той же функциональностью, если завернуть его в Singleton:</p>
66
<p>А вот как выглядит код с той же функциональностью, если завернуть его в Singleton:</p>
67
public class AppConfig { private static AppConfig instance = new AppConfig(); private String apiKey = "default_key"; private String baseUrl = "https://api.example.com"; private AppConfig() { // Приватный конструктор } public static AppConfig getInstance() { return instance; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } } // Использование AppConfig config = AppConfig.getInstance(); System.out.println(config.getApiKey()); // Выведет: default_key System.out.println(config.getBaseUrl()); // Выведет: https://api.example.com // Изменение настроек через синглтон config.setApiKey("new_key"); config.setBaseUrl("https://new-api.example.com"); // Проверка изменённых настроек System.out.println(config.getApiKey()); // Выведет: new_key System.out.println(config.getBaseUrl()); // Выведет: https://new-api.example.com<p>Нужно прописать конструктор и создать экземпляр, если мы хотим вытащить какое-то значение.</p>
67
public class AppConfig { private static AppConfig instance = new AppConfig(); private String apiKey = "default_key"; private String baseUrl = "https://api.example.com"; private AppConfig() { // Приватный конструктор } public static AppConfig getInstance() { return instance; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } } // Использование AppConfig config = AppConfig.getInstance(); System.out.println(config.getApiKey()); // Выведет: default_key System.out.println(config.getBaseUrl()); // Выведет: https://api.example.com // Изменение настроек через синглтон config.setApiKey("new_key"); config.setBaseUrl("https://new-api.example.com"); // Проверка изменённых настроек System.out.println(config.getApiKey()); // Выведет: new_key System.out.println(config.getBaseUrl()); // Выведет: https://new-api.example.com<p>Нужно прописать конструктор и создать экземпляр, если мы хотим вытащить какое-то значение.</p>
68
<p>Код со статическим классом гораздо короче и понятнее. Но он - не панацея. У него есть свои недостатки.</p>
68
<p>Код со статическим классом гораздо короче и понятнее. Но он - не панацея. У него есть свои недостатки.</p>
69
<p>Статический класс предоставляет меньше гибкости для настройки экземпляра. Синглтон может включать нестатические методы и переменные. Это позволяет динамично взаимодействовать с экземпляром.</p>
69
<p>Статический класс предоставляет меньше гибкости для настройки экземпляра. Синглтон может включать нестатические методы и переменные. Это позволяет динамично взаимодействовать с экземпляром.</p>
70
<p>Подход со статическим классом может не во всех языках программирования работать так же, как в Java или C++. Реализация часто меняется в зависимости от контекста и возможностей языка.</p>
70
<p>Подход со статическим классом может не во всех языках программирования работать так же, как в Java или C++. Реализация часто меняется в зависимости от контекста и возможностей языка.</p>
71
<p>Выбор между Singleton и статическим классом зависит от конкретного случая. Вот некоторые рекомендации, которые помогут вам сделать выбор. Синглтон следует использовать в тех случаях, когда:</p>
71
<p>Выбор между Singleton и статическим классом зависит от конкретного случая. Вот некоторые рекомендации, которые помогут вам сделать выбор. Синглтон следует использовать в тех случаях, когда:</p>
72
<p><strong>Требуется гибкость.</strong>Если нужно легко изменять состояние экземпляра, добавлять методы для сложной логики и внедрять зависимости, выбирайте синглтон.</p>
72
<p><strong>Требуется гибкость.</strong>Если нужно легко изменять состояние экземпляра, добавлять методы для сложной логики и внедрять зависимости, выбирайте синглтон.</p>
73
<p><strong>Нужна потокобезопасность.</strong>В многопоточной среде, где нужен ровно один экземпляр класса, выбирайте потокобезопасный синглтон.</p>
73
<p><strong>Нужна потокобезопасность.</strong>В многопоточной среде, где нужен ровно один экземпляр класса, выбирайте потокобезопасный синглтон.</p>
74
<p><strong>Использование настроек и состояний.</strong>Для класса с состояниями или настройками, которые должны меняться динамически, выбирайте синглтон.</p>
74
<p><strong>Использование настроек и состояний.</strong>Для класса с состояниями или настройками, которые должны меняться динамически, выбирайте синглтон.</p>
75
<p>Статический класс используем в двух случаях:</p>
75
<p>Статический класс используем в двух случаях:</p>
76
<p><strong>Требуется хранить константы.</strong>Если ваш класс содержит только константы и статические методы, лучше не мудрить и использовать статический класс.</p>
76
<p><strong>Требуется хранить константы.</strong>Если ваш класс содержит только константы и статические методы, лучше не мудрить и использовать статический класс.</p>
77
<p><strong>Если класс не должен иметь изменяемого состояния</strong>и должен быть независим от конкретных экземпляров.</p>
77
<p><strong>Если класс не должен иметь изменяемого состояния</strong>и должен быть независим от конкретных экземпляров.</p>
78
<p>В конечном счёте выбор зависит от требований вашего проекта, гибкости, которую вы хотите иметь, и особенностей языка программирования, на котором вы работаете.</p>
78
<p>В конечном счёте выбор зависит от требований вашего проекта, гибкости, которую вы хотите иметь, и особенностей языка программирования, на котором вы работаете.</p>
79
<a>Курс с трудоустройством: "Профессия Разработчик + ИИ" Узнать о курсе</a>
79
<a>Курс с трудоустройством: "Профессия Разработчик + ИИ" Узнать о курсе</a>