Есть ли способ разрешить наследование внутри общих шаблонов спецификаций?

c# #entity-framework #generics #entity-framework-core #specification-pattern

#c# #entity-framework #общие #entity-framework-core #спецификация-шаблон

Вопрос:

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

Я использую шаблон спецификации для выполнения фильтрации БД с помощью Entity Framework и избегаю делать это в памяти (я примерно следил за этой статьей). Мой базовый класс спецификации выглядит примерно так:

     public abstract class Specification<T> : ISpecification<T>{

    public abstract Expression<Func<T, bool>> FilterExpr();

    public bool IsSatisfied(T entity)
    {
        Func<T, bool> func = this.FilterExpr().Compile();
        return func(entity);
    }

    public Specification<T> And(Specification<T> otherSpec)
    {
        return new CombinedSpecification<T>(this, otherSpec);
    }
}
 

Из этого базового класса спецификаций выводятся несколько строго типизированных спецификаций, которые хорошо работают сами по себе. Однако проблема возникает при попытке объединить спецификации унаследованных типов. Например, допустим, у меня есть следующие модели:

 public abstract class Person
{
    public int Age {get; set;}
    public string Name {get; set;}
}

public class Baby:Person
{
    public bool CanTalk {get; set;} 
}
 

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

 public class NameSpec : Specification<Person>
    {
        private string name;

        public Namespec(string aName)
        {
            this.name = aName;
        }

        public override Expression<Func<Person, bool>> FilterExpr()
        {
            return p => p.Name == this.name;
        }
    }

public class IsAbleToTalkSpec : Specification<Baby>
    {
        public override Expression<Func<Baby, bool>> FilterExpr()
        {
            return p => p.CanTalk == true;
        }
    }
 

Итак, наконец, допустим, я хочу отфильтровать каждого ребенка по имени Джон, который может говорить:

 var johnSpec = new NameSpec("John");
var combinedSpecification = johnSpec.And(new IsAbleToTalkSpec());

List<Baby> result = myRepository.Find(combinedSpecification); 
 

Несмотря на то, что мои модели должным образом привязаны к БД через конфигурацию EF, это приводит к ошибке компиляции, потому что Specification<Baby> Specification<Person> при их объединении невозможно преобразовать a в a, несмотря на упомянутое наследование. Я понимаю, почему это происходит, но я понятия не имею, как решить эту проблему, не создавая NameSpec<Baby> вместо повторного NameSpec<Person> использования, которое ужасно масштабируется по мере роста моих моделей. Кроме того, вот мой CombinedSpecification<T> класс для справки:

 internal class CombinedSpecification<T> : Specification<T>
{
    private Specification<T> leftSpec;
    private Specification<T> rightSpec;

    public CombinedSpecification(Specification<T> aSpec, Specification<T> otherSpec)
    {
        this.leftSpec = aSpec;
        this.rightSpec = otherSpec;
    }

    public override Expression<Func<T, bool>> FilterExpr()
    {
        var parameter = this.leftSpec.Parameters[0];

        var combined = Expression.AndAlso(
            leftSpec.Body,
            rightSpec.Body.ReplaceParam(rightSpec.Parameters[0], parameter)
        );

        return Expression.Lambda<Func<T, bool>>(combined, parameter);
    }
}
 

Заранее спасибо, что нашли время, чтобы прочитать этот длинный бессвязный текст, надеюсь, я достаточно ясно описал свою проблему.

Комментарии:

1. Нет никакого способа … Specification<Baby> не используется Specification<Person> … даже если вы попытаетесь использовать ковариацию или контравариантность, потому что вы не можете использовать их оба

2. Вы не обязаны использовать абстрактные / базовые классы — они являются только помощниками по реализации. Создайте функциональность для контравариантного интерфейса (например ISpecification<in T> , в вашем примере) и методов расширения. Разница в том, что интерфейсы поддерживают различия, а классы — нет.

3. … и он закончил interface ISpecification<T> { ISpecification<T> And(ISpecification<T> otherSpec); } бы … чего нельзя достичь с помощью ковариантного интерфейса

4. @Selvin Моя ошибка, я имел в виду contra . So ISpecification<Base> может использоваться как ISpecification<Derived> . Аналогично IComparer<T> или IEqualityComparer<T> . And / Or и подобные комбинаторы могут быть просто методами расширения, не обязательно быть частью интерфейса.

5. это также невозможно сделать с контравариантностью… у ISpecification<T> And(ISpecification<T> otherSpec); вас есть как out, так и in…

Ответ №1:

Дизайн вашего класса противоречит тому, чего вы хотите достичь с его помощью.

Используемый вами общий тип определяет тип объекта, который вы в него передаете. Это тот тип, с которым вы выбрали для работы. Но затем вы хотите передать разные (подтипы) типы и автоматически заставить их преобразовать базовый тип в производный тип. Это просто не то, что язык допускает, даже если отбросить обобщения (за исключением неявных преобразований, которые здесь неуместны).

С общей точки зрения ООП, когда вы передаете данные с использованием базового типа:

 public void DoStuff(Person p)
{
    // logic
}
 

Эта внутренняя логика может работать только в предположении, что p это a Person . Хотя это возможно, это обычно указывает на плохой дизайн ООП, и в большинстве случаев его следует избегать.

Вы бы этого не сделали:

 public void DoStuff(object o)
{
    var p = o as Person;
}
 

И поэтому вы тоже не должны этого делать:

 public void DoStuff(Person p)
{
    var b = p as Baby;
}
 

Принцип в точности тот же.

Даже если вы используете общие шаблоны, вы действительно делаете то же самое здесь. Точно так же, как я определил тип параметра моего метода в приведенном выше фрагменте, вы определяете общий тип. В любом случае, как только мы выбрали базовый тип, мы должны работать с этим заданным базовым типом и не должны пытаться незаметно перейти к производному типу.


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

Один из способов сделать ваш код более работоспособным — это указать ограничение универсального типа. Это позволяет вам использовать подтипы, когда вам это нужно.

 public class NameSpec<T> : Specification<T> where T : Person
{
    private string name;

    public Namespec(string aName)
    {
        this.name = aName;
    }

    public override Expression<Func<T, bool>> FilterExpr()
    {
        return p => p.Name == this.name;
    }
}

// If you want to avoid peppering your codebase with <Person> generic 
// types, you can still create a default implementation.
// This allows you to use the non-generic class when dealing with
// Person objects, and use the more specific generic class when you
// are interested in using a more specific subtype.

public class Namespec : Namespec<Person> { }
 

Обратите внимание на where T : Person ограничение. Мы сделали этот класс универсальным, и вызывающей стороне разрешено выбирать общий тип, с которым они работают, но мы установили, что им разрешено выбирать только общие типы, которые являются производными от Person .

Базовое использование было бы:

 var person = new Person() { Name = "Fred" };
var personNameSpec = new Namespec<Person>("Fred");

Assert.IsTrue(personNameSpec.IsSatisfied(person));

var baby = new Baby() { Name = "Pebbles" };
var babyNameSpec = new Namespec<Baby>("Bamm-Bamm");

Assert.IsFalse(babyNameSpec.IsSatisfied(baby));
 

Приведенная выше логика работала бы без включения универсального типа Namespec , поскольку вы могли бы это сделать personNameSpec.IsSatisfied(baby); . Это еще не самая крутая часть.

Вот что самое интересное: поскольку babyNameSpec это a Namespec<Baby> , следовательно, это подтип Specification<Baby> , а не Specification<Person> подобный personNameSpec is!

Это решает проблему слияния двух спецификаций, поскольку универсальными типами теперь являются оба Baby и, следовательно, больше нет Person Baby столкновения / type .

 Specification<Baby> ableToTalkSpec = new IsAbleToTalkSpec();
Specification<Baby> babyNameSpec = new Namespec<Baby>("Bamm-Bamm");

CombinedSpecification<Baby> combinedSpec = ableToTalkSpec.And(babyNameSpec);

var baby = new Baby() { Name = "Pebbles" };

Assert.IsFalse(combinedSpec.IsSatisfied(baby));
 

Комментарии:

1. из вопроса: я понятия не имею, как решить эту проблему, не создавая NameSpec<Baby> вместо повторного использования NameSpec<Person> … так что, вероятно, OP знает об этом решении

2. @Selvin: хотя он все еще использует общий параметр, исходная NameSpec<Person> логика фактически используется здесь повторно, что и было основной целью, согласно цитате, на которую вы ссылались. Мне также неясно, знает ли OP об ограничениях универсального типа или они предлагали создать вторую спецификацию имени, для младенцев, что по сути одно и то же. Я согласен с вами в том, что Namespec<Baby> синтаксис технически предполагает только первое, но общий контекст утверждения OP заставляет меня чувствовать, что он больше склоняется к последнему, отсюда и моя разработка.

3. Я попробую это в ближайшее время и вернусь со своими результатами, спасибо, что нашли время, чтобы подробно объяснить это. Как вы сказали, я упростил вопрос, чтобы показать свою проблему, прошу прощения за все детали, которые я недостаточно четко сформулировал, чтобы прояснить. Знаете ли вы, как ваше решение будет работать с преобразованиями LinQ-> SQL, выполняемыми EF? Кроме того, как можно было бы решить мой первоначальный вопрос, избегая наследования? Заранее спасибо

4. @gst: предполагая, что ваши тела выражений избегают несовместимых вещей, которые EF не может перевести (что уже должно быть в вашем старом коде), все должно быть в порядке. В конце концов, ваш код на самом деле просто работает со стандартными объектами выражения. Насколько я вижу, мое решение не влияет на это.

5. @gst: Избегание наследования здесь — большая тема. Это потребовало бы гораздо большей детализации контекста и гораздо большей детализации ваших целей. Это переопределяет основу, на которую опирается код этого вопроса.