#c_sharp #net #архитектура
Допустим, есть дженерик-интерфейс(IRepository) репозитория с типичными CRUD операциями. Есть дженерик класс CachedModelRepository :IRepository , который принимает на вход в конструкторе этот интерфейс репозитория, сохраняет его в поле и выполняет всякие кэширующие операции. Ну т.е перед тем, как дернуть реализацию интерфейса, он пытается достать данные из кеша. Т.е простая оболочка над интерфейсомю И вот хочется добавить новое поведение. Например, какой-то реализации репозитория нужны доп методы, которые другим не нужны. Я делаю конкретное наследование ISuperRepository:IRepository и добавляю эти методы. И теперь вопрос в том, как грамотно воспользоваться дженерик-классом кешем, зная, что те дженерик-CRUD операции не поменялись. Сначала думал, воспользоваться композицией, но в этом случае я не получу доступ к внутреннем protected словарю. В итоге я сделал ссылку на репозиторий в классе-кеша в виде виртуального свойства: protected virtual IRepository _repository { get; set; } И вот так вот стал работать в наследниках SuperCachedRepository : CachedModelRepository ,ISuperRepository: private ISuperRepository _SuperRepository; protected override IRepository _repository { get => __SuperRepository; set=> _SuperRepository=(ISuperRepository)value; } Т.е родительский класс работает с базовым интерфейсом, а дочерний класс с производным интерфейсом. Так вообще делают? Как-то не совсем естественно смотрится...
Ответы
Ответ 1
В принципе уже есть такой ответ выше, но я бы хотел немножечко расписать поподробней и сделать супер репозиторий также обобщенным. Например, интерфейс public interface IRepository{ void Foo(T item); } Пример репозиитория public class Repository : IRepository { public void Foo(T item) { Console.WriteLine("REPO!"); } } Кешу-декоратору тип репозитория также отдельно пропишем public class Cache : IRepository where T:IRepository { protected virtual T _repository {get;set;} public Cache(T repository) { _repository = repository; } public virtual void Foo(K item) { Console.WriteLine("Cache!"); _repository.Foo(item); } } Супер репозиторий тоже сделаем обобщенным декоратором public class SuperCache : Cache where T : IRepository { public SuperCache(T repository) : base(repository) { } public override void Foo(K item) { Console.WriteLine("SUPER!"); _repository.Foo(item); } } Ну и теперь можно строить цепочки декораторов var repo = new Repository (); var cache = new Cache , int>(repo); var superCache = new SuperCache , int>(cache); superCache.Foo(15); Что выведет SUPER! Cache! REPO! Ответ 2
Делают и так. Подходы к проблеме существуют разные. Мы, из-за того, что используем DI и применяем Autofac, остановились на решении с интерцептором из аспектно-ориентированного-программирования. Решение в Autofac основано на такой интересной штуке, как DynamicProxy из Castle Project, так что их можно использовать и в Castle, и наверное без больших сложностей, с другими IoC фреймворками. Суть решения в том, что мы реализуем кеширование, как декоратор к репозиторию. interface IRepository{ . . . T GetById(Guid id); . . . } public class CacheRepository : IRepository { private readonly IRepository _repository; private readonly IMemoryCache _memoryCache; public CacheRepository(IRepository repository, IMemoryCache memoryCache) { _repository = repository; _memoryCache = memoryCache; } public T GetById(Guid id) { return _memoryCache.GetOrCreate(id, item => _repository.GetById((Guid)item.Key)); } } Конечно, в таком виде нам приходится писать очень много кода, особенно, если у нас методы не только обобщённые, но и специфические в разных репозитория. Именно здесь нас и спасают аспекты. class CacheInterceptor : IInterceptor { private readonly IMemoryCache _memoryCache; public CacheInterceptor(IMemoryCache memoryCache) { _memoryCache = memoryCache; } public void Intercept(IInvocation invocation) { if (invocation.Method.Name == "GetById") { var key = invocation.Arguments[0]; invocation.ReturnValue = _memoryCache(key, item => { invocation.Proceed(); return invocation.ReturnValue; }); } } } Теперь этот класс надо зарегистрировать в Autofac как интерцептор и использовать при регистрации любых репозиториев, как IRepository , так и его наследников. Код получился не очень большой. В случае необходимости метод Intercept можно расширять. Недостатком кода можно считать другой уровень сложности, и то, что теперь надо следовать жёстким соглашениям об именовании методов, которые не проверяются компилятором. Возможно, такой взгляд поможет вам посмотреть на проблему с другой точки зрения. Может быть, подскажет решение, которое лучше вам подойдёт. Ответ 3
Для начала выкинем из вопроса все лишнее. У нас есть интерфейс и его расширение: interface IFoo {} interface IFooEx : IFoo {} И есть базовый потребитель интерфейса, который необходимо расширить так, чтобы получить доступ к расширению интерфейса: class Bar { protected IFoo foo; } class BarEx : Bar { // ??? } Для этого существует несколько основных способов. Способ первый - просто приведение типа Это не самый быстрый способ, но и не самый медленный: он заведомо быстрее любых упражнений с перехватчиками/аспектами или рефлексией. class Bar { protected IFoo Foo { get; set; } } class BarEx : Bar { protected new IFooEx Foo { get { return (IFooEx)base.Foo; } set { base.Foo = value; } } } Самое главное при таком способе - убедиться, что не существует открытых способов записать что-то в свойство базового класса, ведь любой такой способ приведет к нарушению LSP при подобном наследовании. К примеру, нельзя подобное свойство делать открытым и изменяемым: class Bar { // неправильно public IFoo Foo { get; set; } } void Baz(Bar bar) { bar.Foo = new FooImpl(); // БАБАХ, всё поломается когда сюда передадут BarEx } Но это не единственный способ все поломать: class Bar { protected IFoo Foo { get; set; } // неправильно public static void Baz(Bar bar) { bar.Foo = new FooImpl(); // лучше не стало } } Самое надежное - сделать свойство неизменяемым, а инициализировать его в конструкторе - тут уж точно LSP нарушен не будет: class Bar { public IFoo Foo { get; } public Bar(IFoo foo) { Foo = foo; } } Способ второй - абстрактное свойство Этот способ не имеет особых преимуществ перед первым, но он больше соответствует принципу "abstract or sealed", запрещающему наследование конкретных классов. abstract class BarBase { protected abstract IFoo Foo { get; } } sealed class Bar : BarBase { protected override IFoo Foo { get; } } sealed class BarEx : BarBase { private IFooEx foo; protected override IFoo Foo => foo; } В недостатки этого способа можно записать невозможность перекрыть (new) свойство в классе BarEx, поскольку в языке C# свойство не может быть одновременно переопределено и перекрыто в одном и том же классе. Способ третий - обобщенный базовый класс Этот способ хорош вроде бы всем, но всё портит количество угловых скобок, которое в дальнейшем будет только расти... class Barwhere TFoo : IFoo { protected TFoo foo; } class Bar : Bar {} class BarEx : Bae {} Ответ 4
Возможно, с неопределенным типом для репозитория в женерик классе будет легче : public class CachedModelRepository: IRepository where TRep : IRepository { protected TRep _repository { get; set; } public CachedModelRepository(TRep repository) { _repository = repository; } } в этом случае дочерняя инициализация будет выглядеть так: public class SuperCachedRepository : CachedModelRepository , ISuperRepository { public SuperCachedRepository(ISuperRepository repository) : base(repository) { } }
Комментариев нет:
Отправить комментарий