Замена типа в наследнике на производный тип

121
03 февраля 2021, 23:40

Допустим, есть дженерик-интерфейс(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; }

Т.е родительский класс работает с базовым интерфейсом, а дочерний класс с производным интерфейсом. Так вообще делают? Как-то не совсем естественно смотрится...

Answer 1

В принципе уже есть такой ответ выше, но я бы хотел немножечко расписать поподробней и сделать супер репозиторий также обобщенным.

Например, интерфейс

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!
Answer 2

Делают и так. Подходы к проблеме существуют разные.

Мы, из-за того, что используем 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 можно расширять. Недостатком кода можно считать другой уровень сложности, и то, что теперь надо следовать жёстким соглашениям об именовании методов, которые не проверяются компилятором.

Возможно, такой взгляд поможет вам посмотреть на проблему с другой точки зрения. Может быть, подскажет решение, которое лучше вам подойдёт.

Answer 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 Bar<TFoo> where TFoo : IFoo
{
    protected TFoo foo;
}
class Bar : Bar<IFoo> {}
class BarEx : Bae<IFooEx> {}
Answer 4

Возможно, с неопределенным типом для репозитория в женерик классе будет легче :

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)
    {
    }
}
READ ALSO
Как запустить ASP .NET Core проект на сервере с Ubuntu 16.04 и Apach?

Как запустить ASP .NET Core проект на сервере с Ubuntu 16.04 и Apach?

Всем приветДолго искал информацию по этому поводу, но так и не справился с этой задачей

102
Mysql и Lazarus

Mysql и Lazarus

Какой код для лазаруса нужно написать , чтобы потом при запросе символы поменялись на UTF-8?

164
Проблемы с выводом данных из бд через php

Проблемы с выводом данных из бд через php

Мне надо вывести все столбцы из бд, но если столбец повторяется то его не выводитьНапример:

97