IoC
Возможно, вы видели эту штуку под названием IoC. Что это за загадочная и неудобная для набора аббревиатура? Вы попали по адресу.
Что такое IoC?
IoC расшифровывается как Inversion of Control (инверсия управления). То, что делает наш IoCManager — более конкретно Dependency Injection (внедрение зависимостей). По своей сути Dependency Injection означает, что вместо того, чтобы говорить «я хочу именно этот конкретный менеджер логов», вы просто говорите «я хочу штуку, которая может логировать сообщения за меня».
Как это работает? Интерфейсы C#! В примере выше вы бы не обращались к статическому LogManager, вместо этого вы бы сказали IoC получить что угодно, что реализует ILogManager, для вашего использования. Это создаёт более чистый, простой для тестирования и более поддерживаемый код.
Система довольно проста в использовании, если вы знаете её суть (почему вы это и читаете!), так что не волнуйтесь.
Как мне это использовать?
Прежде всего, IoCManager является static, поэтому его можно использовать откуда угодно.
В использовании IoC есть две настоящих части. Создание зависимости и получение её. Говоря об IoC, «зависимость» — это то, чем он управляет.
Регистрация зависимости.
Все зависимости регистрируются вручную при запуске программы, то есть в Program.cs для клиента и сервера и в SS14UnitTest.cs для проекта модульных тестов. IoC сопоставляет тип, обычно интерфейс, с конкретной реализацией, которую он может создать. У конкретной реализации может быть несколько интерфейсов, но у интерфейса может быть только одна реализация. Это делается с помощью метода Register<TInterface, TImplementation>(bool overwrite = false) у IoC Manager, где TImplementation : class, TInterface, new(). По сути, это означает, что вы передаёте ему два типа, второй будет создан, но должен наследовать или реализовывать первый. Где регистрируются эти типы, зависит от того, находитесь ли вы в контенте/сервере/клиенте и т. д., но всё это в единых организованных местах. ClientIoC.cs (движок), ClientContentIoC.cs и т. д. Вы должны уловить закономерность.
Итак, чтобы создать зависимость, вам понадобятся две вещи: интерфейс и реализация. Обычно интерфейс называют так же, как реализацию, но с приставкой I, следующей соглашению C#. Так что, если бы я хотел создать новую зависимость на сервере, я бы сделал так:
// Content.Server/Example/MyDependency.cs
namespace Content.Server.Example;
public class MyDependency : IMyDependency
{
public void Foo()
{
Console.WriteLine("Hello World!");
}
}
// Content.Server/Interfaces/Example/IMyDependency.cs
namespace Content.Server.Interfaces.Example;
public interface IMyDependency
{
/// <summary>
/// Записывает сообщение в консоль.
/// </summary>
void Foo();
}
// ServerContentIoC.cs
IoCManager.Register<IMyDependency, MyDependency>();
И вы сделали свою зависимость доступной всему миру. Теперь как её получить? Просто!
Получение зависимости.
Самый простой способ получить зависимость — использовать IoCManager.Resolve<T>, где T — это интерфейс, использованный в вызове Register<>. В нашем примере выше Resolve<IMyDependency>() вернул бы экземпляр MyDependency, и этот экземпляр всегда будет одним и тем же и общим для всей программы.
Итак, скажем, наш пример хочет логировать, когда вызывается Foo(). Просто!
public class MyDependency : IMyDependency
{
public void Foo()
{
ILogger logger = IoCManager.Resolve<ILogger>();
logger.info("Hi!");
Console.WriteLine("Hello World!");
}
}
Вот так просто.
Однако это некрасиво. Это довольно муторно, и есть серьёзная проблема циклических зависимостей, а ещё метод даже не работает внутри конструкторов зависимостей. Есть решение, конечно:
Внедрение полей и вы
Чтобы решить проблемы с Resolve<T>, IoC Manager может «внедрять» зависимости в поля. Это означает, что две зависимости могут ссылаться друг на друга без проблем. Использовать это тоже довольно просто. IoC Manager находит все поля, помеченные атрибутом [Dependency] (полностью игнорируя readonly и private), и разрешает тип поля. Это делается после выполнения конструкторов всех зависимостей. После внедрения полей, если ваша зависимость реализует IPostInjectInit, будет вызван IPostInjectInit.PostInject(), если вам нужно взаимодействовать с вашими зависимостями. Учтите, что в большинстве случаев у вас всё ещё должен быть где-то явный вызов инициализации, так как этот «псевдоконструктор» подвержен состояниям гонки.
Итак, возьмём наш пример сверху. Давайте переведём его на наш превосходный метод. Мы также добавим лог-сообщение всякий раз, когда тип создаётся, хотя на практике вам следует воздерживаться от этого:
public class MyDependency : IMyDependency, IPostInjectInit
{
[Dependency]
private readonly ILogger logger = default!;
// Вызывается, когда logger становится доступен.
public void PostInject()
{
logger.info("MyDependency being created!");
// ВАЖНО: На самом деле не делайте именно это с логгером. Это передаёт суть, но сломано.
// Поскольку logger ещё не будет правильно инициализирован, у него нет выходного файла.
// Поэтому это сообщение пойдёт в консоль, но не будет залогировано ни в один файл. Это баг.
}
public void Foo()
{
// Это, конечно, нормально, при условии, что `Foo()` вызывается после того, как `BaseServer` сделает свои дела по настройке.
logger.info("Hi!");
Console.WriteLine("Hello World!");
}
}
Внедрение полей можно запускать вручную с помощью IoCManager.InjectDependencies(object). Оно выполняется автоматически для многих динамически создаваемых типов, таких как компоненты сущностей, системы сущностей и всё, что создано через IDynamicTypeFactory.