4.1Kпросмотров
25 августа 2024 г.
Score: 4.5K
Ковариантность и контрвариантность Наткнулся я тут недавно на очередное извращение в одном из рабочих проектов (ибо несколько официальных работ, по nda никто не притянет 😅). Но об этом чуть ниже, сейчас рассмотрим вообще, что такое ковариантность и контрвариантность, а потом на примере, как данные концепции можно применить на примере паттерна CQRS. Ковариантность позволяет использовать более конкретный производный тип, чем указанный тип-параметр. Ковариантность в интерфейсах применяется с использованием ключевого слова out, которое обозначает, что тип-параметр может использоваться как возвращаемое значение. Если же в приведенном примере не использовать out, получим ошибку.
ICovariant<Animal> shelter = new DogShelter();
Animal animal = shelter.GetSomething(); public interface ICovariant<out T>
{ T GetSomething();
} public class Animal { }
public class Dog : Animal { } public class DogShelter : ICovariant<Dog>
{ public Dog GetSomething() => new Dog();
} Контрвариантность позволяет использовать более базовый тип, чем указанный тип-параметр. Контрвариантность применяется с помощью ключевого слова in, которое указывает, что тип-параметр может использоваться как параметр метода.
IContravariant<Dog> handler = new AnimalHandler();
handler.DoSomething(new Dog()); public interface IContravariant<in T>
{ void DoSomething(T value);
} public class Animal { }
public class Dog : Animal { } public class AnimalHandler : IContravariant<Animal>
{ public void DoSomething(Animal value) { Console.WriteLine(value.GetType().Name); }
} Таким образом, если еще упростить: 👉 Ковариантность (out) позволяет использовать производные типы для обобщенных параметров на выходе (возвращаемые значения). 👉 Контрвариантность (in) позволяет использовать базовые типы для обобщенных параметров на входе. CQRS (Command Query Responsibility Segregation) разделяет операции с данными приложения на команды и запросы. В CQRS команды (commands) изменяют состояние системы, а запросы (queries) извлекают данные, но не изменяют их. То есть данный паттерн проектирования позволяет удобно поделить данную часть бизнесс-логики, смысла по этому поводу еще что-то говорить, не вижу. Команды и запросы принимают параметры, которые описывают действие, и эти параметры могут быть более общими, чем конкретные типы, с которыми работает система. Поэтому здесь ложится концепция контрвариантности.
public interface IAsyncQuery<in TQueryModel, TResult>
{ Task<TResult> ExecuteAsync(TQueryModel model, CancellationToken ct = default);
} public class GetRequestStatus : IAsyncQuery<Guid, RequestStatus>
{ private readonly ReadOnlyContext _context; public GetRequestStatus(ReadOnlyContext context) { _context = context; } protected async Task<RequestStatus> ExecuteCoreAsync(Guid id, CancellationToken ct) { RequestEntity request = await _context.RequestEntities .FirstAsync(r => r.Id == id, ct); return request.Status; }
} Что делать, если у нас появилась команда/кверя, куда не нужно ничего передавать?! Товарищи разрабсы с моего проекта решили это очень "элегантно" - создали класс EmptyModel, которая ничего не содержит и передавали ее объект каждый раз)))) Не надо так делать, просто создаем еще IAsyncQruery без передаваемого значения и реализуем данный интерфейс.
public interface IAsyncQuery<in TQueryModel, TResult>
{ Task<TResult> ExecuteAsync(TQueryModel model, CancellationToken ct = default);
} public interface IAsyncQuery<TResult>
{ Task<TResult> ExecuteAsync(CancellationToken ct = default);
} public class GetAllRequests : IAsyncQuery<List<RequestEntity>>
{ private readonly ReadOnlyContext _context; public GetRequestStatus(ReadOnlyContext context) { _context = context; } protected async Task<List<RequestEntity>> ExecuteCoreAsync(CancellationToken ct) { List<RequestEntity> requests = await _context.RequestEntities .AsNoTracking(