Допустим, есть дженерик-интерфейс(IRepository<T>
) репозитория с типичными CRUD
операциями.
Есть дженерик класс CachedModelRepository<T>:IRepository<T>
, который принимает на вход в конструкторе этот интерфейс репозитория, сохраняет его в поле и выполняет всякие кэширующие операции. Ну т.е перед тем, как дернуть реализацию интерфейса, он пытается достать данные из кеша. Т.е простая оболочка над интерфейсомю
И вот хочется добавить новое поведение. Например, какой-то реализации репозитория нужны доп методы, которые другим не нужны. Я делаю конкретное наследование ISuperRepository:IRepository<SomeType>
и добавляю эти методы.
И теперь вопрос в том, как грамотно воспользоваться дженерик-классом кешем, зная, что те дженерик-CRUD операции не поменялись. Сначала думал, воспользоваться композицией, но в этом случае я не получу доступ к внутреннем
protected
словарю.
В итоге я сделал ссылку на репозиторий в классе-кеша в виде виртуального свойства:
protected virtual IRepository<T> _repository { get; set; }
И вот так вот стал работать в наследниках SuperCachedRepository : CachedModelRepository<SuperType>,ISuperRepository
:
private ISuperRepository _SuperRepository;
protected override IRepository<SomeType> _repository { get => __SuperRepository; set=> _SuperRepository=(ISuperRepository)value; }
Т.е родительский класс работает с базовым интерфейсом, а дочерний класс с производным интерфейсом. Так вообще делают? Как-то не совсем естественно смотрится...
В принципе уже есть такой ответ выше, но я бы хотел немножечко расписать поподробней и сделать супер репозиторий также обобщенным.
Например, интерфейс
public interface IRepository<T>
{
void Foo(T item);
}
Пример репозиитория
public class Repository<T> : IRepository<T>
{
public void Foo(T item)
{
Console.WriteLine("REPO!");
}
}
Кешу-декоратору тип репозитория также отдельно пропишем
public class Cache<T, K> : IRepository<K> where T:IRepository<K>
{
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<T, K> : Cache<T, K> where T : IRepository<K>
{
public SuperCache(T repository) : base(repository)
{
}
public override void Foo(K item)
{
Console.WriteLine("SUPER!");
_repository.Foo(item);
}
}
Ну и теперь можно строить цепочки декораторов
var repo = new Repository<int>();
var cache = new Cache<IRepository<int>, int>(repo);
var superCache = new SuperCache<IRepository<int>, int>(cache);
superCache.Foo(15);
Что выведет
SUPER!
Cache!
REPO!
Делают и так. Подходы к проблеме существуют разные.
Мы, из-за того, что используем DI и применяем Autofac, остановились на решении с интерцептором из аспектно-ориентированного-программирования.
Решение в Autofac основано на такой интересной штуке, как DynamicProxy из Castle Project, так что их можно использовать и в Castle, и наверное без больших сложностей, с другими IoC фреймворками.
Суть решения в том, что мы реализуем кеширование, как декоратор к репозиторию.
interface IRepository<T>
{
. . .
T GetById(Guid id);
. . .
}
public class CacheRepository<T> : IRepository<T>
{
private readonly IRepository<T> _repository;
private readonly IMemoryCache _memoryCache;
public CacheRepository(IRepository<T> 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<T>
, так и его наследников. Код получился не очень большой. В случае необходимости метод Intercept
можно расширять. Недостатком кода можно считать другой уровень сложности, и то, что теперь надо следовать жёстким соглашениям об именовании методов, которые не проверяются компилятором.
Возможно, такой взгляд поможет вам посмотреть на проблему с другой точки зрения. Может быть, подскажет решение, которое лучше вам подойдёт.
Для начала выкинем из вопроса все лишнее. У нас есть интерфейс и его расширение:
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 Bar<TFoo> where TFoo : IFoo
{
protected TFoo foo;
}
class Bar : Bar<IFoo> {}
class BarEx : Bae<IFooEx> {}
Возможно, с неопределенным типом для репозитория в женерик классе будет легче :
public class CachedModelRepository<T, TRep> : IRepository<T>
where TRep : IRepository<T>
{
protected TRep _repository { get; set; }
public CachedModelRepository(TRep repository)
{
_repository = repository;
}
}
в этом случае дочерняя инициализация будет выглядеть так:
public class SuperCachedRepository : CachedModelRepository<SomeType, ISuperRepository>, ISuperRepository
{
public SuperCachedRepository(ISuperRepository repository) : base(repository)
{
}
}
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Всем приветДолго искал информацию по этому поводу, но так и не справился с этой задачей
Какой код для лазаруса нужно написать , чтобы потом при запросе символы поменялись на UTF-8?
Мне надо вывести все столбцы из бд, но если столбец повторяется то его не выводитьНапример: