类型不共享基类的泛型类型的多态性

时间:2016-09-27 09:00:47

标签: c# generics expression-trees

背景

这是一个重构问题。我有一堆方法或多或少具有完全相同的代码,但它们作用于不同的类型。每种类型基本上有一种方法,我想将它们全部组合成一个可以使用泛型类型的方法。

当前代码

以下代码可能有助于解释我正在尝试的内容 -

以下方法主要在DbSet<>中有所不同。实体论证。在方法代码中,它们大多使用完全相同的属性,但在一行或两行中,它们可能使用实体类型不共享的属性。例如,AccountId(来自Account实体)和CustomerId(来自Customer实体)。

int? MethodToRefactor(DbSet<Account> entity, List someCollection, string[] moreParams)
        {
            int? keyValue = null;
            foreach (var itemDetail in someCollection)
            {
                string refText = GetRefTextBySource(itemDetail, moreParams);
//Only the below two lines differ in all MethodToRefactor because they use entity's properties that are not shared by all entities
                if (entity.Count(a => a.Name == refText) > 0)
                    keyValue = entity.Where(a => a.Name == refText).First().AccountId;
                if (...some conditional code...)
                    break;
            }
            return keyValue;
        }

int? MethodToRefactor(DbSet<Customer> entity, List someCollection, string[] moreParams)
{
            int? keyValue = null;
            foreach (var itemDetail in someCollection)
            {
                string refText = GetRefTextBySource(itemDetail, moreParams);
//Only the below two lines differ in all MethodToRefactor because they use entity's properties that are not shared by all entities
                if (entity.Count(c => c.CustomerName == refText) > 0)
                    keyValue = entity.Where(c => c.CustomerName == refText).First().CustomerId;
                if (...some conditional code...)
                    break;
            }
            return keyValue;
        }

以下是调用上述方法的代码 -

void Caller()
        {
                    foreach (var entity in EntityCollection)
                    {
                        if (entity.Name == "Account")
                        {
                            id = MethodToRefactor(db.Accounts,...);
                        }
                        else if (entity.Name == "Customer")
                        {
                            id = MethodToRefactor(db.Customers,...);
                        }
            }
    }

问题

这不可扩展,因为它需要为每个新添加的实体复制/粘贴新的MethodToRefactor。它也很难维护。我可以在一个单独的方法中重构所有MethodToRefactors共同的代码,并在每个实体内部执行ifelse,但后来我基本上将调用者与MethodToRefactor合并。我正在寻找一种更简洁的解决方案,其中Caller方法的变化很小,如下所述。

理想/所需的重构代码

这是通用/模板类型的绝佳选择。如下所示,我可以将实际实体更改为通用T,并将实体中不使用公共属性的两行作为表达式/方法传递。

下面是C#类型的伪代码,它演示了理想的解决方案,但我不知道如何在C#中实际执行此操作。

int? MethodToRefactor<T>(DbSet<T> entity, Expression<Func<T, T> filterMethod,
Expression<Func<T, T> getIdMethod, List someCollection, string[] moreParams) where T : Account, Customer //This will fail
{
            int? keyValue = null;
            foreach (var itemDetail in someCollection)
            {
                string refText = GetRefTextBySource(itemDetail, moreParams);
                if (filterMethod(entity) == true)
                    keyValue = getIdMethod(entity);
                if (...some conditional code...)
                    break;
            }
            return keyValue;
        }

void Caller()
        {
                    foreach (var entity in EntityCollection)
                    {
                        if (entity.Name == "Account")
                        {
                            id = MethodToRefactor<Account>(db.Accounts, () => {entity.Count(a => a.Name == refText) > 0}, () => {entity.Where(a => a.Name == refText).First().AccountId},...);
                        }
                        else if (entity.Name == "Customer")
                        {
                            id = MethodToRefactor<Customer>(db.Customer, () => {entity.Count(c => c.CustomerName == refText) > 0}, () => {entity.Where(c => c.CustomerName == refText).First().CustomerId},...);
                        }
            }
    }

实现的好处/目标 1.我们将所有MethodToRefactors合并为一个并消除了所有重复的代码。 我们将实体特定的操作抽象给了调用者。这很重要,因为逻辑被移动到一个逻辑位置,该逻辑位置知道不同实体彼此之间的差异(调用者有一个每个实体ifelse开始)以及如何使用这些差异。 2.通过将实体特定代码委托给调用者,我们也使其更加灵活,这样我们就不必为每个特定于实体的逻辑创建一个MethodToRefactor。

注意:我不是Adapter,Strategy等的忠实粉丝,我更喜欢使用C#语言功能实现这些目标的解决方案。这并不意味着我是反经典设计模式,只是我不喜欢创建一堆新类的想法,因为我可以通过重构几种方法来创建它。

3 个答案:

答案 0 :(得分:2)

如果实体没有相同的基类,那么你能做的最好就是有一个类约束。

由于两个表达式基本相同,因此您应该只传递一个表达式和一个函数来从实体中获取键值。

CountFirst方法也可以合并为一个语句,然后检查null

int? MethodToRefactor<T>(DbSet<T> entities, Func<string, Expression<Func<T, bool>>> expressionFilter, Func<T, int> getIdFunc, IList<string> someCollection, string[] moreParams)
    where T : class
{
    int? keyValue = null;
    foreach (var itemDetail in someCollection)
    {
        string refText = GetRefTextBySource(itemDetail, moreParams);
        var entity = entities.FirstOrDefault(expressionFilter(refText));
        if (entity != null)
        {
            keyValue = getIdFunc(entity);
        }
        if (...some conditional code...)
            break;
    }
    return keyValue;
}

你可以这样调用这个方法

id = MethodToRefactor<Account>(db.Accounts, txt => a => a.Name == txt, a => a.AccountId, ...);
id = MethodToRefactor<Customer>(db.Customers, txt => c => c.CustomerName == txt, c => c.CustomerId, ...);

答案 1 :(得分:1)

以下是如何做到这一点。

给定类型struct Foo { int xxx() {return 0;} void xxx(int){} }; int main() { static_assert(has_xxx<Foo>::value, ""); } ,我们需要的是T属性的访问者与string进行比较,以及refText属性的访问者int 1}}。第一个用keyValue表示,第二个用Expression<Func<T, string>> nameSelector表示,因此这些应该是Expression<Func<T, int>> keySelector的附加参数。

实施怎么样,代码

MethodToRefactor

可以更优化(使用单个数据库查询只返回一个字段),如下所示(伪代码):

if (entity.Count(a => a.Name == refText) > 0)
     keyValue = entity.Where(a => a.Name == refText).First().AccountId;

keyValue = entity.Where(e => nameSelector(e) == refText) .Select(e => (int?)keySelector(e)) .FirstOrDefault(); 不存在时,需要int?强制转换以允许返回null

为了实现这一点,我们需要从参数派生的两个表达式:

refText

Expression<Func<T, bool>> predicate = e => nameSelector(e) == refText;

当然,上述内容不是有效的语法,但可以使用Expression<Func<T, int?>> nullableKeySelector = e => (int?)keySelector(e); 轻松构建。

尽管如此,重构的方法可能是这样的:

System.Linq.Expressions

和用法:

帐户:

int? MethodToRefactor<T>(
    DbSet<T> entitySet,
    Expression<Func<T, string>> nameSelector,
    Expression<Func<T, int>> keySelector,
    List someCollection,
    string[] moreParams)
    where T : class
{
    int? keyValue = null;
    foreach (var itemDetail in someCollection)
    {
        string refText = GetRefTextBySource(itemDetail, moreParams);

        // Build the two expressions needed
        var predicate = Expression.Lambda<Func<T, bool>>(
            Expression.Equal(nameSelector.Body, Expression.Constant(refText)),
                nameSelector.Parameters);

        var nullableKeySelector = Expression.Lambda<Func<T, int?>>(
            Expression.Convert(keySelector.Body, typeof(int?)),
            keySelector.Parameters);

        // Execute the query and process the result
        var key = entitySet.Where(predicate).Select(nullableKeySelector).FirstOrDefault();
        if (key != null)
            keyValue = key;

        if (...some conditional code...)
            break;
    }
    return keyValue;
}

客户:

id = MethodToRefactor(db.Accounts, e => e.Name, e => e.AccountId, ...);

答案 2 :(得分:1)

我知道你没有基类,但你的方法肯定只适用于你的dal类。因此,我将使用接口标记可用的类。这将有助于团队中的其他人了解他们可以使用您的方法的位置。我总是为我的dal类添加一个基本接口。

我认为定义关键属性不是调用者的责任。关键是实体应该提供的东西。

有了一个界面,你可以使用

将密钥属性抽象给它
internal interface IEntity
{
    int Key { get; }
}

当然,如果您有多个密钥类型,则可以通过密钥类型使其具有通用性。

至于您的搜索字词属性,这是您需要决定的内容。要么它也是实体的属性(如果此属性/ ies(为什么只有一个???)在多个地方使用),或者仅在此方法中使用。我想为了简单起见,这只是在这里使用。

在这种情况下,您的方法如下:

int? MethodToRefactor<T>(EfContext context, IEnumerable<Expression<Func<T, string>>> searchFields, IEnumerable<string> someCollection, string[] moreParams)
    where T : class, IEntity
{
    int? keyValue = null;
    foreach (var itemDetail in someCollection)
    {
        string refText = GetRefTextBySource(itemDetail, moreParams);
        if (searchFields.Any())
        {
            var filter = searchFields.Skip(1).Aggregate(EqualsValue(searchFields.First(), refText), (e1, e2) => CombineWithOr(e1, EqualsValue(e2, refText)));
            var entity = context.Set<T>().FirstOrDefault(filter);
            if (entity != null)
            {
                keyValue = entity.Key;
            }
            if (... some condition ...)
                break;
        }
    }
    return keyValue;
}

private Expression<Func<T, bool>> EqualsValue<T>(Expression<Func<T, string>> propertyExpression, string strValue)
{
    var valueAsParam = new {Value = strValue}; // this is just to ensure that your strValue will be an sql parameter, and not a constant in the sql
         // this will speed up further calls by allowing the server to reuse a previously calculated query plan
         // this is a trick for ef, if you use something else, you can maybe skip this
    return Expression.Lambda<Func<T, bool>>(
        Expression.Equal(propertyExpression.Body, Expression.MakeMemberAccess(Expression.Constant(valueAsParam), valueAsParam.GetType().GetProperty("Value"))), 
        propertyExpression.Parameters); // here you can cache the property info
}

private class ParamReplacer : ExpressionVisitor // this i guess you might have already
{
    private ParameterExpression NewParam {get;set;}
    public ParamReplacer(ParameterExpression newParam)
    {
        NewParam = newParam;
    }
    protected override Expression VisitParameter(ParameterExpression expression)
    {
        return NewParam;
    }
}

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> e1, Expression<Func<T, bool>> e2) // this is also found in many helper libraries
{
    return Expression.Lambda<Func<T, bool>>(Expression.Or(e1.Body, new ParamReplacer(e1.Parameters.Single()).VisitAndConvert(e2.Body, MethodBase.GetCurrentMethod().Name)), e1.Parameters);
}

现在这显然要求你在所有实体上实现key属性,在我看来这并不是一件坏事。显然你也会将你的关键属性用于其他东西(否则为什么这个方法只返回一个键)。

另一方面,您在找到匹配项时检索整个实体,但之后您只关心该密钥。通过仅检索密钥,例如,可以使这更好。将select添加到表达式的末尾。不幸的是,在这种情况下,你需要更多的&#34;魔法&#34;为了让ef(或你的linq提供者)理解.Select(e =&gt; e.Key)表达式(至少ef赢得了开箱即用)。因为我希望你需要整个实体在......某些条件......&#34;,我不在这个答案中包括这个版本(也是为了保持简短:P)。

所以最后你的来电者看起来像是:

 void Caller()
    {
                foreach (var entity in EntityCollection)
                {
                    if (entity.Name == "Account")
                    {
                        id = MethodToRefactor<Account>(db, new [] {a => a.Name}, ...);
                    }
                    else if (entity.Name == "Customer")
                    {
                        id = MethodToRefactor<Customer>(db, new [] {c => c.FirstName, c => c.LastName}, ...);
                    }
        }
}