Ответ
При реализации простого DI-контейнера "с нуля" я бы сосредоточился на следующих ключевых методах и принципах:
1. Регистрация сервисов (Registration)
Register<TService, TImplementation>()— регистрация сопоставления типа интерфейса/абстракции с конкретной реализацией.RegisterSingleton<TService>(TService instance)— регистрация готового экземпляра как синглтона.RegisterInstance<TService>(Func<IServiceProvider, TService> factory)— регистрация фабрики для создания экземпляра.
2. Разрешение зависимостей (Resolution)
TService GetService<TService>()— основной метод для получения экземпляра сервиса.object GetService(Type serviceType)— нетипизированная версия для динамических сценариев.- Внутри этого метода контейнер должен рекурсивно обходить граф зависимостей, создавая все необходимые экземпляры.
3. Управление жизненным циклом (Lifetime Management)
Контейнер должен поддерживать три основных скоупа:
- Transient — новый экземпляр при каждом запросе.
- Scoped — один экземпляр на область видимости (например, на web-запрос). Требует реализации
IServiceScope. - Singleton — один экземпляр на весь жизненный цикл контейнера.
Пример упрощённой реализации
public class SimpleContainer
{
private readonly Dictionary<Type, ServiceDescriptor> _descriptors = new();
public void RegisterTransient<TService, TImplementation>() where TImplementation : TService
{
_descriptors[typeof(TService)] = new ServiceDescriptor(typeof(TImplementation), ServiceLifetime.Transient);
}
public TService GetService<TService>()
{
return (TService)GetService(typeof(TService));
}
private object GetService(Type serviceType)
{
if (!_descriptors.TryGetValue(serviceType, out var descriptor))
throw new InvalidOperationException($"Service {serviceType.Name} is not registered.");
// Для синглтона может потребоваться кэширование созданного экземпляра
var implementationType = descriptor.ImplementationType;
var constructor = implementationType.GetConstructors().First();
var parameters = constructor.GetParameters()
.Select(p => GetService(p.ParameterType)) // Рекурсивное разрешение
.ToArray();
return constructor.Invoke(parameters);
}
}
public class ServiceDescriptor
{
public Type ImplementationType { get; }
public ServiceLifetime Lifetime { get; }
// ... конструктор и свойства
}
Важное замечание: В реальных проектах почти всегда лучше использовать проверенные библиотеки (Microsoft.Extensions.DependencyInjection, Autofac, Ninject), которые уже решают сложные проблемы: циклические зависимости, валидацию графа, производительность и интеграцию с различными фреймворками. Самописный контейнер оправдан только для очень специфических учебных или изолированных задач.
Ответ 18+ 🔞
Слушай, а вот это реально интересная тема, про которую все такие умные разговаривают, а на деле-то всё просто, как три копейки. Реализовать свой DI-контейнер — это как собрать табуретку: вроде понятно, но если халтурить, то на пятой точке сидеть будет неудобно, а то и вовсе хуй сломаешь.
Давай по полочкам разложу, как это в голове устроено, без всякой академической пиздобратии.
Первое, что ему нужно — это куда-то всё записывать. Как в блокнотик у бабы Зины на ресепшене: «Вася — сантехник, Петя — электрик». Только у нас вместо людей — типы. Завёлся какой-нибудь ILogger — записали, что когда его попросят, надо отдавать ConsoleLogger. Это называется регистрация.
Методы для этого нужны простые, чтоб даже менеджер понял:
Register<TService, TImplementation>()— классика. Говорим: «Когда будут спрашивать абстракцию (TService), лепи вот эту конкретную реализацию (TImplementation)».RegisterSingleton<TService>(TService instance)— а это когда у тебя уже есть готовый, налаженный экземпляр. Отдаём его же каждый раз, как синглтон. Экономия, блядь, ресурсов.RegisterInstance<TService>(Func<IServiceProvider, TService> factory)— а тут мы уже умничаем. Говорим: «Слушай, дружок-контейнер, когда тебе понадобится эта штука, вот тебе инструкция (фабрика), как её сварганить». Полезно, если создание объекта — это не простоnew, а целый ритуал с плясками.
Второй момент — это выдача всего этого добра наружу. Ты же не просто так всё регистрировал, ты потом это всё захочется получить! Это разрешение зависимостей.
Основной метод — TService GetService<TService>(). Заходишь в него, а там магия начинается. Контейнер лезет в свой «блокнотик», находит запись про ILogger, видит, что для ConsoleLogger нужен ещё какой-то ISettingsProvider в конструкторе. И пошёл-поехал, рекурсивно, как ёбаный крот, копает этот граф зависимостей, пока всё не соберёт. Главное — циклических зависимостей не допустить, а то будет бесконечная рекурсия и пиздец, в стековерфлоу упрёшься.
Третий кирпич в этом здании — управление жизненным циклом. Вот это уже для взрослых. Три основных режима:
- Transient — «одноразовый». Каждый раз, когда просишь, тебе лепят свеженький экземпляр. Как пластиковые стаканчики.
- Scoped — «в рамках одной операции». Один экземпляр на какой-то осмысленный кусок работы (например, на один HTTP-запрос). Это уже посерьёзнее, нужно уметь создавать эти самые «области видимости» (
IServiceScope). - Singleton — «один на всех». Создали один раз в начале и потом тыкаем всем в руки ссылку на него. Экономно, но нужно аккуратнее с состоянием, а то все потоки в него писать начнут.
А вот тебе, смотри, упрощённый набросок, как это внутри может выглядеть. Чисто для понимания, в продакшн такое не тащи, там подводных камней — овердохуища.
public class SimpleContainer
{
// Наш "блокнотик". Тип сервиса -> его описание.
private readonly Dictionary<Type, ServiceDescriptor> _descriptors = new();
// Записали transient-сервис
public void RegisterTransient<TService, TImplementation>() where TImplementation : TService
{
_descriptors[typeof(TService)] = new ServiceDescriptor(typeof(TImplementation), ServiceLifetime.Transient);
}
// Главный метод — "дай-ка сюда!"
public TService GetService<TService>()
{
return (TService)GetService(typeof(TService));
}
// А вот тут и происходит вся кухня
private object GetService(Type serviceType)
{
// Ищем запись в блокноте. Нету? Ну извини, дружок.
if (!_descriptors.TryGetValue(serviceType, out var descriptor))
throw new InvalidOperationException($"Service {serviceType.Name} is not registered.");
// Смотрим, какой конкретно тип нужно создавать
var implementationType = descriptor.ImplementationType;
// Берём первый конструктор (упрощение, в реальности их может быть несколько)
var constructor = implementationType.GetConstructors().First();
// А теперь самое интересное: смотрим, что ему нужно для работы (параметры конструктора),
// и РЕКУРСИВНО идём выпрашивать эти зависимости у самого себя!
var parameters = constructor.GetParameters()
.Select(p => GetService(p.ParameterType))
.ToArray();
// Всё собрали? Ну теперь создаём объект и отдаём.
return constructor.Invoke(parameters);
}
}
// Просто класс, чтобы хранить информацию о сервисе
public class ServiceDescriptor
{
public Type ImplementationType { get; }
public ServiceLifetime Lifetime { get; }
// ... тут ещё можно хранить фабрику или готовый инстанс для синглтона
}
И вот теперь, блядь, самое важное замечание, которое многие игнорируют, а потом чешут репу. В 99.9% реальных проектов нет ни одной, сука, вменяемой причины городить это велосипедово. Серьёзно. Возьми Microsoft.Extensions.DependencyInjection (он уже в ASP.NET Core), Autofac или Ninject. Эти ребята уже обожглись на всех возможных граблях: циклические зависимости, производительность, валидация графа при старте, интеграция с кучей фреймворков. Они это сделали за тебя.
Свой контейнер писать — это либо чисто академический интерес, «а смогу ли я?», либо какая-то абсолютно дикая, изолированная задача, где нельзя тащить лишние либы. Во всех остальных случаях — не еби себе мозг, используй готовое.