Уявіть, що ви розробляєте систему логування для масштабного додатку. Кожен компонент системи має записувати логи у файл, але якщо кожен компонент створюватиме власний екземпляр Logger, це призведе до:
Singleton (Одинак) — це породжуючий патерн проектування, який гарантує існування лише одного екземпляру класу і надає глобальну точку доступу до нього.
| Сценарій | Обґрунтування |
|---|---|
| Logger / Система логування | Один централізований сервіс для запису логів |
| Configuration Manager | Один об'єкт конфігурації для всієї програми |
| Database Connection Pool | Управління обмеженою кількістю з'єднань |
| Cache Manager | Єдине сховище кешованих даних |
| Device Drivers | Один драйвер для доступу до апаратного пристрою |
Singleton (Одинак) — це породжуючий патерн проектування, який гарантує, що клас має лише один екземпляр, і надає глобальну точку доступу до цього екземпляру. Патерн делегує відповідальність за контроль створення самому класу, а не зовнішньому коду.
- instance: Singleton — статичне приватне поле, яке зберігає єдиний екземпляр- Singleton() — приватний конструктор (позначено -), заборонений для зовнішніх викликів+ GetInstance(): Singleton — публічний статичний метод (позначено + та $), єдина точка доступуnew, лише через GetInstance()| Учасник | Роль | Відповідальність |
|---|---|---|
| Singleton | Клас-одинак | Забезпечує єдиний екземпляр, контролює доступ через статичний метод |
| Client | Клієнт | Отримує екземпляр через GetInstance(), не може створити новий через new |
namespace DesignPatterns.Creational.Singleton;
/// <summary>
/// Наївна реалізація Singleton (НЕ потокобезпечна)
/// </summary>
public class Logger
{
// Статичне поле для зберігання єдиного екземпляру
private static Logger? _instance;
// Приватний конструктор заборонює створення через new
private Logger()
{
Console.WriteLine("Logger ініціалізовано");
}
// Публічна точка доступу до екземпляру
public static Logger GetInstance()
{
// ПРОБЛЕМА: Якщо два потоки одночасно увійдуть сюди,
// обидва можуть створити новий екземпляр
if (_instance == null)
{
_instance = new Logger();
}
return _instance;
}
public void Log(string message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
}
// Отримуємо екземпляр Logger (створюється при першому виклику)
var logger1 = Logger.GetInstance();
logger1.Log("Перше повідомлення");
// Отримуємо той самий екземпляр
var logger2 = Logger.GetInstance();
logger2.Log("Друге повідомлення");
// Перевірка, що це той самий об'єкт
Console.WriteLine($"logger1 == logger2: {ReferenceEquals(logger1, logger2)}"); // True
namespace DesignPatterns.Creational.Singleton;
/// <summary>
/// Thread-safe реалізація Singleton з Double-Checked Locking
/// </summary>
public sealed class ConfigurationManager
{
private static ConfigurationManager? _instance;
// Об'єкт для синхронізації потоків
private static readonly object _lock = new();
public Dictionary<string, string> Settings { get; }
private ConfigurationManager()
{
Console.WriteLine("Configuration Manager ініціалізовано");
Settings = new Dictionary<string, string>
{
["AppName"] = "My Application",
["Version"] = "1.0.0"
};
}
public static ConfigurationManager GetInstance()
{
// Перша перевірка (без блокування) — для продуктивності
if (_instance == null)
{
// Блокування — лише якщо екземпляр ще не створений
lock (_lock)
{
// Друга перевірка (всередині блокування) — для безпеки
// Потрібна, бо інший потік міг створити екземпляр,
// поки ми чекали на lock
if (_instance == null)
{
_instance = new ConfigurationManager();
}
}
}
return _instance;
}
public string GetSetting(string key)
{
return Settings.TryGetValue(key, out var value) ? value : "Not found";
}
}
if (_instance == null) виконується без блокування для підвищення продуктивності. Якщо екземпляр вже створений, блокування не відбувається.lock (_lock) — критична секція, де тільки один потік може перебувати одночасно.sealed: Клас позначений як sealed, щоб заборонити успадкування (це важливо для Singleton, бо наслідування може порушити гарантію єдиності екземпляру).if (_instance == null).// Багатопоточне середовище
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
int threadId = i;
tasks.Add(Task.Run(() =>
{
var config = ConfigurationManager.GetInstance();
Console.WriteLine($"Thread {threadId}: {config.GetSetting("AppName")}");
}));
}
await Task.WhenAll(tasks);
// Виведе "Configuration Manager ініціалізовано" лише ОДИН раз
Lazy<T>, який автоматично забезпечує потокобезпечність та ледаче ініціалізацію.namespace DesignPatterns.Creational.Singleton;
/// <summary>
/// Thread-safe Singleton через Lazy<T> — РЕКОМЕНДОВАНИЙ підхід
/// </summary>
public sealed class DatabaseConnection
{
// Lazy<T> гарантує потокобезпечність та ледаче ініціалізацію
private static readonly Lazy<DatabaseConnection> _lazyInstance =
new(() => new DatabaseConnection());
public static DatabaseConnection Instance => _lazyInstance.Value;
public string ConnectionString { get; }
private DatabaseConnection()
{
Console.WriteLine("Database Connection ініціалізовано");
ConnectionString = "Server=localhost;Database=MyApp;";
}
public void ExecuteQuery(string query)
{
Console.WriteLine($"Виконується запит: {query}");
}
}
| Характеристика | Опис |
|---|---|
| ✅ Потокобезпечність | Lazy<T> автоматично використовує блокування |
| ✅ Ледаче ініціалізацію | Об'єкт створюється лише при першому звертанні до .Value |
| ✅ Мінімалізм коду | Не потрібно писати власну логіку блокування |
| ✅ Продуктивність | Оптимізований механізм блокування всередині .NET |
// Простий доступ через властивість Instance
var db1 = DatabaseConnection.Instance;
db1.ExecuteQuery("SELECT * FROM Users");
var db2 = DatabaseConnection.Instance;
Console.WriteLine($"db1 == db2: {ReferenceEquals(db1, db2)}"); // True
Lazy<T> для реалізації Singleton в C#. Це найпростіший, найбезпечніший та найефективніший підхід. Забудьте про ручне блокування!namespace DesignPatterns.Creational.Singleton;
/// <summary>
/// Singleton з жадібною ініціалізацією (Eager Initialization)
/// </summary>
public sealed class CacheManager
{
// Екземпляр створюється ОДРАЗУ при завантаженні класу
private static readonly CacheManager _instance = new();
public static CacheManager Instance => _instance;
private readonly Dictionary<string, object> _cache;
private CacheManager()
{
Console.WriteLine("Cache Manager ініціалізовано");
_cache = new Dictionary<string, object>();
}
public void Set(string key, object value)
{
_cache[key] = value;
}
public object? Get(string key)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}
| ✅ Підходить | ❌ Не підходить |
|---|---|
| Екземпляр завжди потрібен в програмі | Створення об'єкту дороге за ресурсами |
| Ініціалізація швидка | Екземпляр може не знадобитися взагалі |
| Просто та зрозуміло | Потрібна ледача ініціалізація |
| Підхід | Потокобезпечність | Ледача ініціалізація | Складність | Рекомендація |
|---|---|---|---|---|
| Наївний | ❌ Ні | ✅ Так | Проста | ❌ Уникати |
| Double-Checked Lock | ✅ Так | ✅ Так | Середня | ⚠️ Якщо немає Lazy |
| Lazy | ✅ Так | ✅ Так | Проста | ✅ Рекомендовано |
| Eager Initialization | ✅ Так | ❌ Ні | Проста | ⚠️ Якщо завжди потрібен |
HttpClient — класичний приклад, де Singleton критично важливий. Створення багатьох екземплярів HttpClient призводить до вичерпання портів (socket exhaustion).namespace DesignPatterns.Creational.Singleton;
/// <summary>
/// Singleton для HttpClient — запобігає socket exhaustion
/// </summary>
public sealed class HttpClientSingleton
{
private static readonly Lazy<HttpClient> _lazyClient = new(() =>
{
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30),
BaseAddress = new Uri("https://api.example.com/")
};
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
return client;
});
public static HttpClient Client => _lazyClient.Value;
private HttpClientSingleton() { }
}
// Всі запити використовують один HttpClient
var response1 = await HttpClientSingleton.Client.GetAsync("users");
var response2 = await HttpClientSingleton.Client.GetAsync("posts");
// Не створюються нові TCP з'єднання для кожного запиту
IHttpClientFactory замість Singleton для HttpClient, оскільки фабрика надає додаткові можливості (named clients, typed clients, middleware).using Serilog;
namespace DesignPatterns.Creational.Singleton;
public sealed class LoggerSingleton
{
private static readonly Lazy<ILogger> _lazyLogger = new(() =>
{
return new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
});
public static ILogger Logger => _lazyLogger.Value;
private LoggerSingleton() { }
}
public class UserService
{
public void CreateUser(string username)
{
LoggerSingleton.Logger.Information("Створення користувача: {Username}", username);
// Логіка створення користувача
LoggerSingleton.Logger.Debug("Користувач {Username} створений успішно", username);
}
}
Завжди віддавайте перевагу Lazy<T> для реалізації Singleton в C#.
private static readonly Lazy<MyClass> _instance = new(() => new MyClass());
public static MyClass Instance => _instance.Value;
Заборона успадкування критична для Singleton.
public sealed class MySingleton { }
Це єдиний спосіб заборонити зовнішнє створення екземплярів.
private MySingleton() { }
Для кращої тестованості, розгляньте можливість створення інтерфейсу.
public interface IConfigurationManager { }
public sealed class ConfigurationManager : IConfigurationManager { }
// В Dependency Injection контейнері
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
// АНТИПАТЕРН: Singleton зі змінюваним станом
public class GlobalState
{
public static GlobalState Instance { get; } = new();
public int Counter { get; set; } // Небезпечно!
private GlobalState() { }
}
// Будь-яка частина коду може змінити Counter
GlobalState.Instance.Counter = 42;
public class ImmutableConfig
{
public static ImmutableConfig Instance { get; } = new();
public IReadOnlyDictionary<string, string> Settings { get; }
private ImmutableConfig()
{
Settings = new Dictionary<string, string>
{
["ApiKey"] = "secret"
}.AsReadOnly();
}
}
// АНТИПАТЕРН: Клас жорстко залежить від Singleton
public class UserService
{
public void CreateUser(string name)
{
// Неможливо замінити DatabaseConnection на mock в тестах
DatabaseConnection.Instance.ExecuteQuery($"INSERT INTO Users VALUES ('{name}')");
}
}
// ПРАВИЛЬНО: Залежність передається через конструктор
public class UserService
{
private readonly IDatabaseConnection _db;
public UserService(IDatabaseConnection db)
{
_db = db; // Можна передати mock в тестах
}
public void CreateUser(string name)
{
_db.ExecuteQuery($"INSERT INTO Users VALUES ('{name}')");
}
}
// В тесті
var mockDb = new Mock<IDatabaseConnection>();
var service = new UserService(mockDb.Object);
Microsoft.Extensions.DependencyInjection), який візьме на себе контроль життєвого циклу.// Замість Singleton патерну
services.AddSingleton<IConfigurationManager, ConfigurationManager>();
// Клас більше не контролює своє створення
public class ConfigurationManager : IConfigurationManager
{
public ConfigurationManager() { } // Публічний конструктор
}
var instance1 = MySingleton.GetInstance();
var instance2 = MySingleton.GetInstance();
Console.WriteLine(ReferenceEquals(instance1, instance2)); // False (!)
Lazy<T> або Double-Checked Locking.private static readonly Lazy<MySingleton> _instance = new(() => new MySingleton());
public static MySingleton Instance => _instance.Value;
System.TypeInitializationException: The type initializer for 'MySingleton' threw an exception.
private DatabaseConnection()
{
try
{
ConnectionString = ConfigurationManager.GetConnectionString();
}
catch (Exception ex)
{
Console.WriteLine($"Помилка ініціалізації: {ex.Message}");
throw; // Перекидаємо виняток після логування
}
}
// Зламування Singleton через Reflection
var constructor = typeof(MySingleton).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null, Type.EmptyTypes, null);
var instance = (MySingleton)constructor.Invoke(null); // Новий екземпляр!
public sealed class ProtectedSingleton
{
private static readonly Lazy<ProtectedSingleton> _instance = new(() => new ProtectedSingleton());
public static ProtectedSingleton Instance => _instance.Value;
private ProtectedSingleton()
{
// Захист від Reflection
if (_instance != null && _instance.IsValueCreated)
{
throw new InvalidOperationException("Використовуйте Instance властивість для доступу до Singleton.");
}
}
}
Опис:
Реалізуйте Singleton для класу Logger, який:
Lazy<T> для потокобезпечностіLog(string message, LogLevel level)LogLevel з рівнями: Info, Warning, ErrorОчікуваний результат:
var logger = Logger.Instance;
logger.Log("Програма запущена", LogLevel.Info);
logger.Log("Попередження!", LogLevel.Warning);
// Виведе:
// [22:15:30] INFO: Програма запущена
// [22:15:31] WARNING: Попередження!
Рішення:
namespace DesignPatterns.Practice;
public enum LogLevel
{
Info,
Warning,
Error
}
public sealed class Logger
{
private static readonly Lazy<Logger> _instance = new(() => new Logger());
public static Logger Instance => _instance.Value;
private Logger()
{
Console.WriteLine("Logger ініціалізовано");
}
public void Log(string message, LogLevel level)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
var levelText = level.ToString().ToUpper();
Console.WriteLine($"[{timestamp}] {levelText}: {message}");
}
}
using DesignPatterns.Practice;
var logger = Logger.Instance;
logger.Log("Програма запущена", LogLevel.Info);
logger.Log("Можлива проблема", LogLevel.Warning);
logger.Log("Критична помилка", LogLevel.Error);
// Перевірка, що це Singleton
var logger2 = Logger.Instance;
Console.WriteLine($"Один екземпляр? {ReferenceEquals(logger, logger2)}"); // True
Опис:
Створіть Singleton ConfigurationManager, який:
appsettings.jsonLazy<T> для thread-safetyGetValue(string key) для отримання налаштуваньDictionary<string, string>FileNotFoundException, якщо файл не знайденийФайл appsettings.json:
{
"AppName": "My Application",
"Version": "1.0.0",
"ApiUrl": "https://api.example.com"
}
Очікуваний результат:
var config = ConfigurationManager.Instance;
Console.WriteLine(config.GetValue("AppName")); // My Application
Console.WriteLine(config.GetValue("Version")); // 1.0.0
Рішення:
using System.Text.Json;
namespace DesignPatterns.Practice;
public sealed class ConfigurationManager
{
private static readonly Lazy<ConfigurationManager> _instance =
new(() => new ConfigurationManager());
public static ConfigurationManager Instance => _instance.Value;
private readonly Dictionary<string, string> _settings;
private ConfigurationManager()
{
const string filePath = "appsettings.json";
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Файл конфігурації не знайдено: {filePath}");
}
var json = File.ReadAllText(filePath);
_settings = JsonSerializer.Deserialize<Dictionary<string, string>>(json)
?? new Dictionary<string, string>();
Console.WriteLine($"Завантажено {_settings.Count} налаштувань з {filePath}");
}
public string GetValue(string key)
{
if (_settings.TryGetValue(key, out var value))
{
return value;
}
throw new KeyNotFoundException($"Налаштування '{key}' не знайдено");
}
public IReadOnlyDictionary<string, string> GetAllSettings()
{
return _settings;
}
}
using DesignPatterns.Practice;
try
{
var config = ConfigurationManager.Instance;
Console.WriteLine($"AppName: {config.GetValue("AppName")}");
Console.WriteLine($"Version: {config.GetValue("Version")}");
Console.WriteLine($"ApiUrl: {config.GetValue("ApiUrl")}");
// Спроба отримати неіснуючий ключ
try
{
config.GetValue("NonExistent");
}
catch (KeyNotFoundException ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Помилка: {ex.Message}");
}
Опис:
Реалізуйте Singleton ConnectionPool, який:
GetConnection() для отримання вільного з'єднанняReleaseConnection(DbConnection connection) для повернення з'єднання в пулGetConnection(), якщо всі з'єднання зайняті, доки не звільниться хоча б однеSemaphoreSlim для контролю доступуОчікуваний результат:
var pool = ConnectionPool.Instance;
// В багатопоточному середовищі
var tasks = Enumerable.Range(0, 10).Select(i => Task.Run(async () =>
{
var connection = await pool.GetConnection();
Console.WriteLine($"Thread {i}: отримано з'єднання {connection.Id}");
await Task.Delay(1000); // Симуляція роботи
pool.ReleaseConnection(connection);
Console.WriteLine($"Thread {i}: повернуто з'єднання {connection.Id}");
}));
await Task.WhenAll(tasks);
Рішення:
namespace DesignPatterns.Practice;
/// <summary>
/// Імітація з'єднання з базою даних
/// </summary>
public class DbConnection
{
public int Id { get; }
public bool IsInUse { get; set; }
public DbConnection(int id)
{
Id = id;
IsInUse = false;
}
public void Open()
{
Console.WriteLine($"З'єднання {Id} відкрито");
}
public void Close()
{
Console.WriteLine($"З'єднання {Id} закрито");
}
}
namespace DesignPatterns.Practice;
public sealed class ConnectionPool
{
private static readonly Lazy<ConnectionPool> _instance =
new(() => new ConnectionPool());
public static ConnectionPool Instance => _instance.Value;
private readonly List<DbConnection> _connections;
private readonly SemaphoreSlim _semaphore;
private readonly object _lock = new();
private const int MaxConnections = 5;
private ConnectionPool()
{
_connections = new List<DbConnection>();
_semaphore = new SemaphoreSlim(MaxConnections, MaxConnections);
// Створюємо пул з'єднань
for (int i = 0; i < MaxConnections; i++)
{
var connection = new DbConnection(i + 1);
connection.Open();
_connections.Add(connection);
}
Console.WriteLine($"Connection Pool ініціалізовано з {MaxConnections} з'єднаннями");
}
public async Task<DbConnection> GetConnection()
{
// Чекаємо, доки з'явиться вільне з'єднання
await _semaphore.WaitAsync();
lock (_lock)
{
// Знаходимо перше вільне з'єднання
var connection = _connections.First(c => !c.IsInUse);
connection.IsInUse = true;
return connection;
}
}
public void ReleaseConnection(DbConnection connection)
{
lock (_lock)
{
if (!_connections.Contains(connection))
{
throw new InvalidOperationException("З'єднання не належить цьому пулу");
}
connection.IsInUse = false;
}
// Звільнюємо семафор, дозволяючи іншому потоку отримати з'єднання
_semaphore.Release();
}
public int GetAvailableConnectionsCount()
{
lock (_lock)
{
return _connections.Count(c => !c.IsInUse);
}
}
}
using DesignPatterns.Practice;
var pool = ConnectionPool.Instance;
// Симуляція багатопоточного доступу
var tasks = Enumerable.Range(0, 10).Select(i => Task.Run(async () =>
{
Console.WriteLine($"Thread {i}: запит на з'єднання (доступно: {pool.GetAvailableConnectionsCount()})");
var connection = await pool.GetConnection();
Console.WriteLine($"Thread {i}: отримано з'єднання {connection.Id}");
// Симуляція роботи з БД
await Task.Delay(Random.Shared.Next(500, 2000));
pool.ReleaseConnection(connection);
Console.WriteLine($"Thread {i}: повернуто з'єднання {connection.Id}");
}));
await Task.WhenAll(tasks);
Console.WriteLine($"\nВсі задачі завершені. Доступних з'єднань: {pool.GetAvailableConnectionsCount()}");
Пояснення рішення:
Що таке Singleton?
Коли використовувати?
Як реалізувати в C#?
Головна небезпека
::
| Альтернатива | Коли використовувати |
|---|---|
| Dependency Injection Container | Коли потрібен контроль життєвого циклу зовні класу |
| Static Class | Коли не потрібен стан, лише статичні методи |
| Factory Pattern | Коли потрібна гнучкість створення різних реалізацій |
| Monostate Pattern | Коли потрібна поведінка Singleton без обмеження екземплярів |