Делаем ковариантный интерфейс обратно совместимым - PullRequest
2 голосов
/ 13 марта 2019

У нас есть интерфейс для работы с DAL с довольно простым определением:

interface IRepository<T> : IQueriable<T> // so we can read data from database
{
   Save(T document); // dozen of methods here
} 

В основном мы используем две реализации: реальную версию и версию памяти для модульного тестирования.Вот объявления одного из классов:

public RealRepository : IRepository<AccountEntity> { ... } 
// typical IOC usage
services.AddSingleton<IRepository<AccountEntity>, RealRepository<AccountEntity>>();

Теперь мы работаем над выделением основной базы кода для пользовательской версии проекта, и нам нужны настраиваемые поля в данных и иногда настраиваемое поведение в репозитории.Большинство классов подходят для базовой реализации, но для других потребуется конкретная реализация.Поэтому моя цель состоит в том, чтобы получить следующие сервисы в:

var repository = new RealRepository<CustomAccountEntity>();
services.AddSingleton(IRepository<AccountEntity>, repository);
// for new classes
services.AddSingleton(IRepository<CustomAccountEntity>, repository);

Я пытался добавить out T в IRepository, но я использую T во входных параметрах, и это дало ошибку времени компиляции "Invalid Variance".

Я вижу решение, добавив параметр второго типа в интерфейс, чтобы он выглядел следующим образом:

IRepository<TBase, out TChild> : IQueriable<TChild> {
    Save (T document);
}

Наконец, Вопрос: Как сделать изменение на 100% обратно совместимым?

Что я пробовал:

  1. Добавить IRepository<T>: IRepository<T,T> -> соответствует, но RealRepository больше не реализует IRepository.
  2. Добавить 2 интерфейса в реализацию:public class RealRepository<TBase, TChild>: IRepository<TBase, TChild>, IRepository<TChild> но это дает ошибку комплимента 'не может реализовать оба ... и ..., потому что они могут объединиться для некоторых замен параметров типа'

1 Ответ

1 голос
/ 14 марта 2019

Save(T document) имеет T в контравариантной позиции. Это означает, что in T, не out T.

Давайте вспомним, что означает контрвариантность. Предположим, у вас был этот код:

using System;

public class Entity {}
public class AccountEntity : Entity {}
public class CustomAccountEntity : AccountEntity {}

public interface IQueryable<in T>
    where T : Entity
{}

public interface IRepository<in T>
    where T : Entity
{
    void Save(T record);
}

public class EntityRepository<T> : IRepository<T>
    where T : Entity
{
    public void Save(T record) {}
}

public class Program
{
    public static void Main()
    {
        // This is ***VALID***:
        IRepository<CustomAccountEntity> repo = new EntityRepository<AccountEntity>();
        Console.WriteLine(repo == null ? "cast is invalid" : "cast is valid");
    }
}

https://dotnetfiddle.net/cnEdcm

Поэтому, когда вам нужен IRepository<CustomAccountEntity>, вы можете использовать конкретный экземпляр EntityRepository<AccountEntity>. Кажется нелогичным, но на самом деле это совершенно правильно: если конкретный метод - Save(AccountEntity), он, очевидно, может обрабатывать CustomAccountEntity экземпляров; OTOH, если бы конкретный метод был Save(CustomAccountEntity), он НЕ смог бы обрабатывать простые AccountEntity экземпляры.

Сказав это, тогда я думаю, что вы должны

  1. Вместо этого используйте контравариантность;
  2. Объявите все зависимости, используя наиболее специализированный тип, например, IRepository<CustomWhateverEntity>;
  3. В регистрационном коде IoC для каждого конкретного объекта задайте либо Repository<CustomeWhateverEntity>, если вам нужно дополнительное поведение, либо просто Repository<WhateverEntity> в противном случае.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...