Интерфейсы и абстрактные классы C#
Dependency Management in C# ClassesУправление зависимостями в классах C##
Projects where classes create everything they need by themselves often have common architectural problems, because they handle their own dependencies directly. For example, a class might internally create services like a custom player HUD or input settings. This causes several issues:Проекты, в которых классы создают всё необходимое самостоятельно, часто сталкиваются с архитектурными проблемами, поскольку такие классы напрямую управляют своими зависимостями. Например, класс может создавать внутренние сервисы, такие как пользовательский интерфейс игрока или настройки ввода. Это приводит к ряду проблем:
- Tight Coupling: Using specific implementations directly within a class creates tight connections between the class and those implementations. Modifying or replacing these dependencies, such as changing an input handler from keyboard to gamepad, inevitably requires changing the dependent class itself, violating the open-closed principle.Жёсткая связанность: Когда класс напрямую использует конкретные реализации, между ними возникает тесная связь. Любое изменение зависимости (например, замена обработчика ввода с клавиатуры на геймпад) требует внесения изменений в сам класс, что нарушает принцип открытости/закрытости (Open-Closed Principle).
- Poor Maintainability: Dependency setup logic often becomes duplicated across the application. If multiple components individually configure the same dependency, it becomes difficult to manage and understand the configurations, and thus harder to maintain.Проблемы с поддержкой кода: Логика инициализации зависимостей может дублироваться в разных частях приложения. Если несколько компонентов по-своему настраивают одну и ту же зависимость, становится трудно контролировать и понимать конфигурацию — это усложняет поддержку.
- Difficulty in Unit Testing: Hard-coded dependencies severely limit testability. For example, when a class internally creates a complex dependency like an audio manager, it becomes difficult to replace it with a mock during testing, which makes unit tests less reliable.Проблемы с модульным тестированием: Жестко заданные зависимости серьезно ограничивают возможность тестирования. Например, когда класс внутри себя создает сложную зависимость, вроде аудио-менеджера, во время тестирования становится трудно заменить ее макетом, что делает модульные тесты менее надежными.
- Reduced Reusability and Modularity: Components tied to certain dependencies become less flexible. This makes it harder to reuse them in different situations. If a component is directly linked to a specific manager or subsystem, you can't easily move it or use it somewhere else.Снижение переиспользуемости и модульности: Компоненты, связанные с конкретными зависимостями, становятся менее гибкими. Это затрудняет их повторное использование в других ситуациях. Если компонент напрямую связан с определенным менеджером или подсистемой, его нельзя просто так перенести или использовать где-то ещё.
Dependency InjectionВнедрение зависимостей#
Dependency Injection (DI) is a way to write code that's flexible and easy to manage. Rather than creating or directly referencing its dependencies, a class receives them from outside. Внедрение зависимостей (DI) - это способ писать гибкий и легко управляемый код. Вместо того чтобы создавать зависимости или напрямую к ним обращаться, класс получает их извне.
Classes depend on an abstraction (an interface or abstract class) rather than a concrete implementation. This approach adheres to the Dependency Inversion Principle (DIP), which states that high-level modules should not depend on low-level modules; both should depend on abstractions. Moreover, abstractions themselves should not depend on details – the details should depend on the abstraction.Классы зависят не от конкретных реализаций, а от абстракций (интерфейсов или абстрактных классов). Этот подход основан на принципе инверсии зависимостей (DIP): модули верхнего уровня не должны зависеть от модулей нижнего уровня - оба должны зависеть от абстракций. Более того, сами абстракции не должны зависеть от деталей – детали должны зависеть от абстракции.
By following DIP, you reduce direct coupling and make it easier to replace components without affecting higher-level logic.Следуя принципу DIP, можно уменьшить связанность компонентов и упростить их замену без влияния на логику более высокого уровня.
To address issues above and build more flexible, testable, and maintainable systems in UNIGINE, you can use components that implement interfaces or inherit from abstract classes. C# Component System supports this approach and makes it easier to program to abstractions rather than concrete implementations and decouple components from specific dependencies.Чтобы избежать перечисленных проблем и создавать более гибкие, тестируемые и поддерживаемые системы в UNIGINE, вы можете использовать компоненты, которые реализуют интерфейсы или наследуются от абстрактных классов. Система компонентов C# поддерживает этот подход и упрощает программирование на основе абстракций, а не конкретных реализаций, тем самым снижая зависимость компонентов от конкретных реализаций.
InterfacesИнтерфейсы#
Interfaces are a mechanism for defining contracts that classes can implement. Any class that implements an interface agrees to provide that functionality. Интерфейсы - это механизм определения контрактов, которые классы могут реализовать. Любой класс, реализующий интерфейс, обязуется предоставить соответствующую функциональность.
Using interfaces for DI is a common pattern to achieve maximum decoupling. The approach to working with interfaces in UNIGINE can be summarized as follows:Использование интерфейсов для внедрения зависимостей является распространенным способом, позволяющим максимально разорвать связи между компонентами. Работа с интерфейсами в UNIGINE строится по следующей схеме:
-
Define an Interface. Определите интерфейс.
Identify the behavior or functionality that needs to be provided, and define an interface declaring the required methods. This interface serves as a contract that any specific component must fulfill.Определите поведение или функциональность, которые необходимо обеспечить, и опишите соответствующий интерфейс с нужными методами. Этот интерфейс станет контрактом, который должен выполняться любым конкретным компонентом.
Исходный код (C#)public interface IShootable { public void Shoot(); }
ПримечаниеYou can create an empty *.cs file right in the Asset Browser window.Вы можете создать пустой файл *.cs прямо в окне Asset Browser. -
Implement the Interface in a Component. Реализуйте интерфейс в компоненте.
Create one or more components that implement this interface. They will contain the actual code to perform the work, but from the outside they will be accessed via the interface. You can have multiple implementations of the same interface coexisting, which is a powerful way to swap behaviors. The component that implements the interface is called a service.Создайте один или несколько компонентов, реализующих этот интерфейс. Эти компоненты будут содержать фактическую логику выполнения, но снаружи к ним будут обращаться через интерфейс. Вы можете иметь несколько реализаций одного и того же интерфейса одновременно — это эффективный способ гибко менять поведение. Компонент, реализующий интерфейс, называется сервисом.
Исходный код (C#)public class WizardStaff : Component, IShootable { public void Shoot() { Log.MessageLine("The wizard's staff shot a fireball."); } } public class MagicWand : Component, IShootable { public void Shoot() { Log.MessageLine("The magic wand shot an electric zap."); } } public class Bow : Component, IShootable { public void Shoot() { Log.MessageLine("The bow shot an arrow."); } }
-
Depend on the Interface in the Client.Используйте интерфейс в клиентском компоненте.
Any component that needs to use the special behavior should not directly instantiate concrete service class. Instead, it should rely on a reference to an interface type. The component that depends on the service is called a client.Любой компонент, которому требуется определённое поведение, не должен напрямую создавать экземпляр конкретного класса сервиса. Вместо этого он должен опираться на ссылку типа интерфейса. Компонент, который использует сервис, называется клиентом.
Исходный код (C#)public class Player : Component { [ShowInEditor] IShootable mainPlayerWeapon = null; [ShowInEditor] IShootable sparePlayerWeapon = null; void Update() { if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT)) { mainPlayerWeapon?.Shoot(); } if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.RIGHT)) { sparePlayerWeapon?.Shoot(); } } }
-
Inject the Concrete Implementation. Внедрите конкретную реализацию.
This step is where the wiring happens. The interface reference in the client must be assigned a concrete object instance that implements the interface. There are a couple of ways to do this using UNIGINE features:На этом этапе происходит "связывание" — ссылке на интерфейс в клиентском компоненте необходимо присвоить экземпляр объекта, реализующего этот интерфейс. В UNIGINE это можно сделать несколькими способами:
-
Via the Editor. In UNIGINE you can expose the interface field in the component parameters. By default, parameters are displayed or hidden in the UI in accordance with access modifiers: public - displayed, otherwise - hidden. But you can show a private or protected one by specifying the corresponding visibility attribute ShowInEditor as shown above in the code sample.Через редактор. В UNIGINE вы можете сделать поле интерфейса доступным в параметрах компонента. По умолчанию параметры отображаются или скрываются в пользовательском интерфейсе в соответствии с модификаторами доступа: public - отображаются, в противном случае - скрыты. Однако можно явно показать приватное или защищённое поле, используя соответствующий атрибут видимости ShowInEditor, как показано в примере кода выше.
Then, you simply assign a node with an assigned component that implements your specific interface.Затем вы просто назначаете ноду с компонентом, который реализует нужный интерфейс.
-
Via the API. Or you can locate the interface using the engine API.Через API. Также вы можете получить ссылку на интерфейс с помощью API движка.
Исходный код (C#)// Getting IShootable var newWeapon = obj.GetComponent<IShootable>(); // If newWeapon is not null, it will be new main player weapon mainPlayerWeapon = newWeapon ?? mainPlayerWeapon;
A common pattern is calling GetComponent<>() method in the client's code. The engine will search for a component that implements the IShootable interface, as in our example, and return a reference to it.Обычно используется шаблон вызова метода GetComponent<>() в коде клиента. Движок выполнит поиск компонента, реализующего интерфейс IShootable, как в нашем примере, и вернет ссылку на него.
The key point is that the client code does not instantiate the service directly - instead, it receives a reference or obtains it from the environment.Ключевой момент: клиентский код не создаёт экземпляр сервиса напрямую. Вместо этого он получает ссылку извне или находит её в окружении.
-
Abstract ClassesАбстрактные классы#
Interfaces aren't the only way to invert dependencies – abstract classes can also serve as the abstraction layer between a client and the concrete implementation. The workflow for using abstract classes is very similar to the interface approach, with a few differences:Интерфейсы – не единственный способ инверсии зависимостей. В качестве абстракции между клиентом и конкретной реализацией также можно использовать абстрактные классы. Подход к использованию абстрактных классов во многом похож на работу с интерфейсами, но есть некоторые отличия:
-
Define an Abstract Base Class. Определите абстрактный базовый класс.
Create an abstract class that declares the necessary methods and optionally provides common functionality or default behavior shared across implementations. Since it's abstract, it can't be instantiated directly - instead, it serves as a template that concrete subclass components must follow.Создайте абстрактный класс, который объявляет необходимые методы и при необходимости содержит общую функциональность или поведение по умолчанию, применимое ко всем реализациям. Поскольку класс абстрактный, он не может быть создан напрямую — вместо этого он служит шаблоном, которому должны соответствовать все конкретные компоненты-наследники.
Исходный код (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public abstract class Toggleable : Component { [ShowInEditor] private bool isToggled = false; public bool Toggled { get => isToggled; set { if (value != isToggled) { bool ok = value ? On() : Off(); isToggled = isToggled ^ ok; } } } public bool Toggle() => isToggled = isToggled ^ (isToggled ? Off() : On()); protected abstract bool On(); protected abstract bool Off(); }
-
Create Subclasses that Inherit from the Abstract Class. Создайте подклассы, наследующиеся от абстрактного класса.
Implement one or more classes that extend the abstract base class. Each subclass must implement the abstract methods, providing its own behavior.Реализуйте один или несколько классов, которые расширяют абстрактный базовый класс. Каждый подкласс должен реализовывать абстрактные методы, предоставляя свою собственную логику поведения.
Исходный код (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class Lamp : Toggleable { [ParameterColor] public vec4 emission_color = vec4.WHITE; protected override bool On() { Log.MessageLine("Lamp::On()"); return SetEmissionColor(emission_color); } protected override bool Off() { Log.MessageLine("Lamp::Off()"); return SetEmissionColor(vec4.ZERO); } private bool SetEmissionColor(vec4 emission_color) { Object obj = (Object)node; if (obj == null) return false; for (var surface = 0; surface < obj.NumSurfaces; surface += 1) obj.SetMaterialParameterFloat4("emission_color", emission_color, surface); return true; } private void Init() { SetEmissionColor(Toggled ? emission_color : vec4.ZERO); } }
Исходный код (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class Fan : Toggleable { public float rotation_speed = 120; private float target_speed = 0; private float actual_speed = 0; protected override bool On() { Log.MessageLine("Fan::On()"); target_speed = rotation_speed; return true; } protected override bool Off() { Log.MessageLine("Fan::Off()"); target_speed = 0; return true; } private void Init() { target_speed = Toggled ? rotation_speed : 0; } private void Update() { actual_speed = MathLib.Lerp(actual_speed, target_speed, Game.IFps); node.Rotate(0, 0, actual_speed * Game.IFps); } }
-
Inject a Concrete Subclass Instance into the Client Class. Добавьте конкретный экземпляр подкласса в клиентский класс.
Similar to the interface case, the client that needs the functionality holds a reference of the abstract base class type. Then, because of polymorphism, you can pass specific implementations of the abstract class to the client class in two following ways:Как и в случае с интерфейсом, клиент, которому требуется определённая функциональность, хранит ссылку на тип абстрактного базового класса. Благодаря полиморфизму вы можете передавать конкретные реализации абстрактного класса клиентскому классу двумя следующими способами:
-
Via the Editor. Simply assign a node with the assigned component, that inherits your abstract base class in a node Parameters window:Через редактор. Просто назначьте ноду с назначенным компонентом, который наследует ваш абстрактный базовый класс, в окне Parameters ноды:
-
Via the API. Retrieving abstract base classes via the API works in a similar way as interfaces.Через API. Получение абстрактных базовых классов через API работает аналогично интерфейсам.
Исходный код (C#)public class Toggler : Component { private void Update() { // Some logic for intersection with togglable object... if (obj) { var toggleable = obj.GetComponent<Toggleable>(); if (toggleable) { toggleable.Toggle(); } } } }
In this case, the client code requests a component that derives from an abstract class. The engine traverses the available components and returns one that matches the expected base type, such as Toggleable.В этом случае клиентский код запрашивает компонент, унаследованный от абстрактного класса. Движок перебирает доступные компоненты и возвращает тот, который соответствует ожидаемому базовому типу, например Toggleable.
-
Choosing the Right Abstraction MechanismВыбор подходящего механизма абстракции#
Interfaces and abstract classes both define behaviour without binding to concrete implementations, but they differ in intent and capabilities:И интерфейсы, и абстрактные классы определяют поведение без привязки к конкретной реализации, но они различаются по назначению и возможностям:
- Interfaces are ideal for defining behaviour that can be implemented by any class, regardless of its place in the hierarchy. They also support multiple inheritance by allowing a class to implement multiple interfaces.Интерфейсы идеально подходят для описания поведения, которое может быть реализовано любым классом, независимо от его места в иерархии. Они также поддерживают множественное наследование, позволяя классу реализовывать сразу несколько интерфейсов.
- Abstract classes are more appropriate when there is a need to share common implementation details, such as fields or methods across a group of related types.Абстрактные классы лучше использовать, когда для группы связанных типов необходима общая реализация, например, каких-либо полей или методов.
It's important to use abstractions when they bring clear benefits - such as easier testing, multiple implementations, or shared logic. But if there's only one concrete use case and no need for reuse or substitution, keeping things simple is often the better choice.Важно использовать абстракции только тогда, когда они действительно дают ощутимые преимущества, например, облегчают тестирование, позволяют создавать несколько реализаций или делиться общей логикой. Но если предполагается лишь один конкретный сценарий использования без переиспользования и замены, то простой прямой подход часто оказывается лучше.
Using External Dependency Injection FrameworksИспользование внешних фреймворков внедрения зависимостей#
You can configure dependencies manually within the engine - using the editor and components to wire things together. This method gives full control over how and when dependencies are resolved. Alternatively, external Dependency Injection frameworks can be used. They allow you to:Вы можете настраивать зависимости вручную внутри движка — используя редактор и компоненты для их связывания. Этот способ даёт полный контроль над тем, когда и как разрешаются зависимости. В качестве альтернативы можно использовать внешние фреймворки, реализующие внедрение зависимостей. Они позволяют:
- Use familiar tools from the .NET ecosystem.использовать знакомые инструменты из экосистемы .NET;
- Simplify dependency management in larger projects.упростить управление зависимостями в более крупных проектах;
- Access advanced DI features like scopes, signals, or event-based injection.получить доступ к расширенным возможностям внедрения зависимостей, таким как области видимости, сигналы или внедрение по событиям.
Below are examples of how to set up some popular external DI frameworks.Ниже приведены примеры того, как настроить некоторые популярные внешние DI-фреймворки.
Setting up Zenject in a UNIGINE ProjectНастройка Zenject в проекте UNIGINE#
- Download the non-Unity build from the official Zenject GitHub repositoryСкачайте сборку не для Unity, с официального репозитория Zenject на GitHub
- Extract the downloaded archived *.dll files into your project's bin folderРаспакуйте архив и скопируйте файлы *.dll в папку bin вашего проекта
-
Add references to the *.dll files in your project:Добавьте ссылки на файлы *.dll в свой проект:
-
Option 1: Using Your IDE Вариант 1: Через IDE
- Open your project in your preferred IDE.Откройте свой проект в вашей среде разработки.
- Open the Reference Manager or the equivalent interface for managing project references.Откройте Reference Manager или аналогичное окно управления зависимостями.
- Use the Browse option to locate the required DLLs in the project's bin folder.Нажмите Browse и укажите путь к DLL-файлам в папке bin проекта.
- Select previously added files.Выберите ранее добавленные файлы.
- Confirm the selection to add them as references in your project.Подтвердите выбор, чтобы добавить их в качестве ссылок в свой проект.
-
Option 2: Manually edit the *.csproj file Вариант 2: Вручную редактировать файл *.csproj
Open your project *.csproj file and add:Откройте файл *.csproj вашего проекта и добавьте следующее:
Исходный код (XML)<ItemGroup> <Reference Include="Zenject"> <HintPath>bin\Zenject.dll</HintPath> </Reference> <Reference Include="Zenject-Signals"> <HintPath>bin\Zenject-Signals.dll</HintPath> </Reference> <Reference Include="Zenject-usage"> <HintPath>bin\Zenject-usage.dll</HintPath> </Reference> </ItemGroup>
-
- Restart your IDE (and Editor, if needed) to ensure the changes are recognized.Перезапустите IDE (и редактор, если необходимо), чтобы изменения были корректно подхвачены.
-
You're now ready to use the Zenject in your project. For usage details, refer to the Zenject documentation.Теперь вы готовы использовать Zenject в своем проекте. Подробности по использованию смотрите в документации Zenject.
Исходный код (C#)public interface ISomeInterface { void Send(string message); } public class SomeImplementation : Component, ISomeInterface { public void Send (string message) { Log.MessageLine(message); } }
Исходный код (C#)using System.Collections; using System.Collections.Generic; using Unigine; using Zenject; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class ClientCode : Component { void Init() { var container = new DiContainer(); container.Bind<ISomeInterface>().To<SomeImplementation>().AsSingle(); var foo = container.Resolve<ISomeInterface>(); foo.Send("Hello, Zenject!"); } }
Setting up Microsoft DI in a UNIGINE ProjectНастройка Microsoft DI в проекте UNIGINE#
- Open your project in your preferred IDE.Откройте свой проект в предпочитаемой среде разработки.
-
Install the NuGet package for Microsoft.Extensions.DependencyInjection:Установите пакет NuGet для Microsoft.Extensions.DependencyInjection:
- Via the built-in IDE NuGet Package Manager, orЧерез встроенный NuGet Package Manager в IDE, или
-
Using the .NET CLI command:С помощью команды .NET CLI:
Исходный кодdotnet add package Microsoft.Extensions.DependencyInjection
-
That's it - Microsoft's DI framework is now available in your project. You can build more complex setups using scopes, lifetimes, and modules - see the official docs for details.Вот и все — DI-фреймворк от Microsoft теперь доступен в вашем проекте. настраивать более сложные сценарии с использованием области видимости, времени жизни и модулей. Подробности смотрите в официальной документации.
Исходный код (C#)using System.Collections; using System.Collections.Generic; using Unigine; using Microsoft.Extensions.DependencyInjection; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class ClientCode : Component { void Init() { var services = new ServiceCollection(); services.AddTransient<ISomeInterface, SomeImplementation>(); var serviceProvider = services.BuildServiceProvider(); var foo = serviceProvider.GetRequiredService<ISomeInterface>(); foo.Send("Hello, .NET DI!"); } }
Информация, представленная на данной странице, актуальна для версии UNIGINE 2.20 SDK.