Cześć! Dziś przyjrzymy się jednemu z fundamentalnych mechanizmów we współczesnym programowaniu obiektowym – Dependency Injection (DI). Jeśli chcesz tworzyć kod, który jest łatwiejszy w utrzymaniu, rozszerzaniu i testowaniu, DI jest kluczem. Zobaczysz, jak w praktyce zastosować tę technikę w C#/.NET, aby już od samego początku swojej przygody wprowadzić do projektów dobre praktyki.
Co to jest Dependency Injection?
Dependency Injection to wzorzec projektowy, który realizuje tzw. odwrócenie zależności (Dependency Inversion Principle – jedna z zasad SOLID). Polega on na tym, by obiekty nie tworzyły bezpośrednio swoich zależności (np. klas współpracujących), ale otrzymywały je z zewnątrz:
• Dzięki temu klasy stają się mniej zależne od konkretnych implementacji i można je łatwiej wymieniać lub testować.
• Kod jest bardziej modułowy i czytelniejszy.
• Implementacje możemy podmieniać (np. używać różnych wariantów tej samej funkcjonalności), co jest szczególnie przydatne podczas pisania testów jednostkowych.
Prosty przykład w C#
Przyjrzyjmy się sytuacji, w której mamy klasę EmailService, wysyłającą wiadomości:
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public class SmtpEmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
Console.WriteLine($"Wysyłanie emaila przez SMTP do: {to}, temat: {subject}");
// Kod wysyłki wiadomości
}
}
Zła praktyka: klasa zależna sama tworzy obiekt SmtpEmailService:
public class UserController
{
private SmtpEmailService _emailService = new SmtpEmailService();
public void RegisterUser(string email)
{
// ... logika rejestracji
_emailService.SendEmail(email, "Witamy", "Dziękujemy za rejestrację!");
}
}
Problem? Gdy będziemy chcieli przetestować UserController, a nie chcemy wysyłać prawdziwych emaili, będziemy mieli trudniej, bo UserController jest mocno związany z SmtpEmailService.
Dobra praktyka: wstrzykujemy (inject) zależność przez konstruktor:
public class UserController
{
private readonly IEmailService _emailService;
public UserController(IEmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string email)
{
// ... logika rejestracji
_emailService.SendEmail(email, "Witamy", "Dziękujemy za rejestrację!");
}
}
Teraz możemy łatwo podmienić IEmailService na np. FakeEmailService podczas testów:
public class FakeEmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
// Udajemy, że wysyłamy email, w rzeczywistości nie robimy nic
Console.WriteLine("Symulacja wysyłki emaila (FAKE).");
}
}
// Przykład użycia
public static void Main()
{
// Wersja produkcyjna
var userController1 = new UserController(new SmtpEmailService());
userController1.RegisterUser("real@user.com");
// Wersja testowa
var userController2 = new UserController(new FakeEmailService());
userController2.RegisterUser("test@mock.com");
}
Wbudowany kontener DI w .NET (przykład w aplikacji konsolowej)
Od .NET Core (i nowszych .NET 5+), mamy do dyspozycji wbudowany kontener DI, z którego można skorzystać nawet w aplikacji konsolowej:
using Microsoft.Extensions.DependencyInjection;
class Program
{
static void Main(string[] args)
{
// Konfiguracja kontenera usług
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IEmailService, SmtpEmailService>();
serviceCollection.AddTransient<UserController>();
var serviceProvider = serviceCollection.BuildServiceProvider();
// Pobieramy gotowy obiekt UserController z wstrzykniętymi zależnościami
var userController = serviceProvider.GetService<UserController>();
userController.RegisterUser("hello@world.com");
}
}
• AddTransient<IEmailService, SmtpEmailService>() – rejestrujemy, że kiedy w konstruktorze jakaś klasa poprosi o IEmailService, dostanie obiekt SmtpEmailService.
• AddTransient<UserController>() – rejestrujemy też sam UserController, aby kontener mógł go utworzyć.
• serviceProvider.GetService<UserController>() – tworzy nam instancję UserController z odpowiednią konfiguracją.
Różne cykle życia
Podczas rejestracji w kontenerze DI możesz wybrać, czy obiekt będzie:
• Transient – nowa instancja przy każdym wywołaniu.
• Scoped – pojedyncza instancja w ramach "zakresu" (np. jednego żądania w ASP.NET Core).
• Singleton – jedna instancja na cały czas życia aplikacji.
Jak to wygląda w ASP.NET Core?
W przypadku projektów ASP.NET Core konstrukcja wygląda podobnie, ale konfigurację DI robimy zwykle w pliku Program.cs (lub dawniej w Startup.cs). Przykład:
var builder = WebApplication.CreateBuilder(args);
// Rejestracja serwisów
builder.Services.AddTransient<IEmailService, SmtpEmailService>();
builder.Services.AddTransient<UserController>();
var app = builder.Build();
app.MapGet("/", (UserController userController) =>
{
userController.RegisterUser("user@example.com");
return "Zarejestrowano!";
});
app.Run();
Czasem wystarczy, że w kontrolerze ASP.NETCore poprosisz o parametry w konstruktorze, a DI sam wstrzyknie odpowiednie zależności.
Podsumowanie
Dependency Injection to nie tylko "modne hasło" – to fundament nowoczesnej architektury aplikacji w .NET. Dzięki niemu kod staje się bardziej modułowy, testowalny i przyjazny dla kolejnych programistów, którzy będą z nim pracować.
Jeśli chcesz nauczyć się więcej na temat dobrych praktyk w .NET, poznać wzorce projektowe, tworzyć aplikacje ASP.NET Core czy pracować z bazami danych – polecam Ci moje kompletne szkolenie online: "Zostań Programistą .NET". Znajdziesz tam kompleksowe omówienie nie tylko Dependency Injection, ale wielu innych kluczowych elementów ekosystemu .NET.
Daj znać w
komentarzach, jakie są Twoje doświadczenia z Dependency Injection i czy
widzisz różnicę w jakości kodu po jego wprowadzeniu.
To wszystkie na dzisiaj. Jeżeli taki artykuł Ci się spodobał, to koniecznie dołącz do mojej społeczności – darmowe zapisy, gdzie będziesz również miał dostęp do dodatkowych materiałów i przede wszystkim bonusów. Do zobaczenia w kolejnym artykule.