Wprowadzanie
W poprzednim wpisie pokazałem, w jaki sposób za pomocą biblioteki Scrutor dodać do wbudowanego kontenera dependency incjection w .NET Core/.NET 5 automatyczną rejestrację typów. Scrutor poza skanowaniem udostępnia również możliwość rejestrowania dekoratorów, za pomocą których możemy nawet w sposób dynamiczny konfigurować zachowanie typów w naszej aplikacji. A to wszystko bez zmiany kodu typu, do którego będziemy dodawali zachowanie.
Wzorzec dekorator
Dekorator jest jednym z wzorców strukturalnych i służy głównie do dynamicznego dodawania dodatkowych zachować do istniejących klas. Za jego pomocą opakowujemy rozszerzany typ, który na ogół jest przekazywany do dekorator jako parametry w konstruktorze. Dekorator dodaje jakieś zachowanie przed/po wywołaniu kodu dekorowanego obiektu. Możemy mieć w systemie wiele dekoratorów, które dodają kolejne zachowania, a następnie możemy komponować je w zależności od potrzeby.
W samym .NET jest trochę typów wykorzystujących wzorzec dekoratora. Przykładem takim są strumienie, gdzie poprzez dodawanie kolejnych strumieni możemy rozszerzyć zachowanie bazowego strumienia. Na przykład otwieramy strumień do pliku, na podstawie którego później dodajemy strumień szyfrujący, strumień kompresujący oraz strumień zapisujący, który umożliwi nam zamianę typów na ciągi bajtów. W zależności od tego, jak udekorujemy sobie bazowy strumień, tak rozszerzymy zachowanie bazowego strumienia.
Innym przykładem są middleware z ASP.NET Core. Za ich pomocą również dekorujemy sposób obsługi żądań w aplikacji. Poprzez dodanie kolejnych middleware możemy dodać takie zachowania jak uwierzytelnianie, autoryzacja, cache i tak dalej.
Przykład dekoratora
W tym wpisie będę bazował na poprzednim wpisie, w którym pokazałem jak użyć biblioteki Scrutor do automatycznej rejestracji typów, szczególnie, że użyjemy tej biblioteki do rejestracji dekoratora.
W przykładzie miałem dodałem prosty interfejs IProductRepository oraz jego implementacje, które wyglądały tak:
public interface IProductRepository : IRepository | |
{ | |
Product GetById(Guid id); | |
} | |
public class ProductRepository : IProductRepository | |
{ | |
public Product GetById(Guid id) | |
{ | |
return new Product() | |
{ | |
Id = id | |
}; | |
} | |
} |
Załóżmy, że chcemy rozszerzyć metodę GetById o logowanie informacji o jej wywołaniu. Równie dobrze mogą to być inne zachowania jak ponawianie operacji, czy cachowanie jej wyniku. Moglibyśmy to dorzucić do tej metody, ale w takich sytuacjach dekorator jest na ogół lepszym rozwiązaniem. Możemy później zmieniać zachowanie w zależności od potrzeb, a i kod poszczególnych dekoratorów będzie prostszy i łatwiejszy do testowania.
Przykładowy dekorator może wyglądać tak:
public class ProductRepositoryLoggerDecorator : IProductRepository | |
{ | |
private readonly IProductRepository _productRepository; | |
private readonly ILogger<ProductRepositoryLoggerDecorator> _logger; | |
public ProductRepositoryLoggerDecorator(IProductRepository productRepository, ILogger<ProductRepositoryLoggerDecorator> logger) | |
{ | |
_productRepository = productRepository; | |
_logger = logger; | |
} | |
public Product GetById(Guid id) | |
{ | |
var repositoryType = _productRepository.GetType(); | |
_logger.LogInformation($"Executing {repositoryType.Namespace} GetById - id: {id}"); | |
var product = _productRepository.GetById(id); | |
_logger.LogInformation($"Executed {repositoryType.Namespace} GetById - id: {id}"); | |
return product; | |
} | |
} |
W kodzie widać kilka charakterystycznych rzeczy dla dekoratora:
- Implementujemy ten sam typ, który rozszerzamy – w tym przypadku dekorator również implementuje IProductRepository
- Do konstruktora przekazujemy typ, który chcemy rozszerzyć, czyli ten sam typ, który implementujemy – tutaj IProductRepository – możemy również przekazywać inne zależności, których potrzebujemy w naszej logice, ta jak tutaj dodatkowo wstrzyknąłem obiekt loggera
- W metodzie, którą rozszerzamy, wywołujemy tę samą metodę z przekazanej zależności – plus jakaś dodatkowa logika
W niektórych przypadkach ostatni punkt jest pomijany – na przykład podczas implementowania cache. Możemy nie wykonać metody z przekazanej zależności, gdy obiekt znajduje się w cache.
Rejestracja dekoratora
Scrutor ułatwia nam rejestrację dekoratora i wygląda ona tak:
public static class ServiceCollectionExtensions | |
{ | |
public static IServiceCollection AddRepositories(this IServiceCollection services) | |
{ | |
services.AddScoped<IProductRepository, ProductRepository>(); | |
services.TryDecorate<IProductRepository, ProductRepositoryLoggerDecorator>(); | |
return services; | |
} | |
} |
Rejestrujemy nasz właściwy typ w kontenerze (za chwilę wrócę jeszcze do automatycznej rejestracji z poprzedniego wpisu). W kolejnym kroku korzystamy z metody TryDecorate, którą dodaje Scutor do wbudowanego kontenera. W metodzie określamy, jaki typ chcemy udekorować (IProductRepository) oraz jakim dekoratorem to robimy (ProductRepositoryLoggerDecorator). Możemy w tym miejscu skonfigurować wiele dekoratorów w zależności na przykład od ustawień aplikacji z pliku konfiguracyjnego.
W tym momencie kontener podczas rozwiązywania IProductRepository (punkt 1 poniżej) utworzy instancje klasy ProductRepositoryLoggerDecorator (punkt 2), do której przekaże instancje klasy ProductRepository (punkt 3), co widać na zrzucie poniżej:
Problem z automatyczną rejestracją
W momencie gdy wrócimy do automatycznej rejestracji, będziemy mieli problem. Jest on spowodowany tym, że mamy dwie implementacje interfejsu IProductRepository, które będą automatycznie zarejestrowane, z czego ProductRepositoryLoggerDecorator na wejściu przyjmuje IProductRepository.
Aby rozwiązać problem, potrzebujemy oznaczyć klasę ProductRepositoryLoggerDecorator i nie rejestrować jej podczas automatycznej rejestracji. Możemy to zrobić za pomocą własnego atrybutu Decorator. Jego definicja jest bardzo prosta:
public class DecoratorAttribute : Attribute | |
{ | |
} |
Nastomiast użycie wygląda tak:
[Decorator] | |
public class ProductRepositoryLoggerDecorator : IProductRepository | |
{ | |
.... | |
} |
W tym momencie możemy wrócić do automatycznej rejestracji, do której dorzucimy drugi warunek w wyborze typów podczas skanowania. Użyjemy metody generycznej WithoutAttribute, dzięki której zarejestrujemy wszystkie typy bez określonego atrybutu. Czyli w naszym przypadku pominięty zostanie ProductRepositoryLoggerDecorator. Poprawiona automatyczna rejestracja wygląda tak:
gist ServiceCollectionExtensions2.cs
Przykład
Na potrzeby tego wpisu rozszerzyłem przykład z poprzedniego wpisu. Znajdziesz go pod adresem https://github.com/danielplawgo/ScrutorTests. Podobnie jak wcześniej nie jest potrzebna jakaś dodatkowa konfiguracja. Można od razu uruchomić przykład.
Podsumowanie
Wzorzec dekorator jest bardzo przydatnym rozwiązaniem. W prosty sposób możemy rozszerzać zachowanie obiektów poprzez dodawanie nowego kodu, czyli mamy ładnie spełnioną regułę otwarte-zamknięte z SOLID.
Natomiast dzięki bibliotece Scrutor możemy łatwo taki dekorator zarejestrować we wbudowanym w .NET Core/.NET 5 kontenerze dependency injection.
1 thought on “Scrutor użycie dekoratora”