有没有办法允许在规范模式泛型中继承?

Is there a way to allow for inheritance inside Specification pattern generics?

首先,对不起问题的标题,很难想出一个措辞,让我解释一下情况。

我正在使用规范模式通过 Entity Framework 执行数据库过滤,并避免在内存中执行此操作 (I roughly followed this article)。我的基本规格 class 是这样的:

    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);
    }
}

从这个基本规范 class 衍生出多个 Strongly-typed 规范,它们各自运作良好。然而,当试图组合继承类型的规范时就会出现问题。例如,假设我有以下型号:

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;
        }
    }

所以最后,假设我想过滤每个名叫 John 会说话的婴儿:

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

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

尽管我的模型已通过 EF 配置正确绑定到数据库,但这样做会导致编译错误,因为在组合它们时无法将 Specification<Baby> 转换为 Specification<Person> ,尽管提到了继承。我理解为什么会发生这种情况,但我不知道如何在不创建 NameSpec<Baby> 而不是重用 NameSpec<Person> 的情况下解决这个问题,随着我的模型的增长,它的规模非常大。另外,这是我的 CombinedSpecification<T> class 供参考:

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);
    }
}

提前感谢您花时间阅读这篇冗长的文章,我希望我在描述我的问题时足够清楚。

您的 class 设计与您的目标相矛盾。

您使用的通用类型决定了您传递给它的对象的类型。那就是您选择与之合作的类型。但是随后您想要传递不同的(子)类型并自动让它们将基类型向上转换为派生类型。即使将泛型放在一边(除非隐式转换,这与此处无关),这也不是语言所允许的。

从一般 OOP 的角度来看,当您使用基本类型传递数据时:

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

内部逻辑只能在 pPerson 的假设下工作。虽然可以向上转型,但这通常表示 OOP 设计不佳,在大多数情况下应避免。

你不会这样做:

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 约束。我们已将此 class 设为泛型,调用者可以选择他们使用的泛型类型,但我们强制要求他们只能选择 是 [=63] 的泛型=] 的 来自 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);。这还不是最酷的部分。

这是很酷的部分:因为 babyNameSpecNamespec<Baby>,因此它是 Specification<Baby> 的子类型,而不是像 personNameSpec 那样的 Specification<Person>是!

这解决了合并两个规范的问题,因为通用类型现在都是 Baby,因此不再存在 Person/Baby 类型冲突。

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));