【问题标题】:Polymorphism by generic types where types do not share a base class泛型类型的多态性,其中类型不共享基类
【发布时间】:2016-09-27 09:00:46
【问题描述】:

背景

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

当前代码

也许下面的代码将有助于解释我正在尝试什么 -

以下方法的主要区别在于 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 合二为一,消除了所有重复代码。 2. 我们将实体特定的操作抽象给调用者。这一点很重要,因为该逻辑被移动到一个逻辑位置,该位置知道不同实体之间的差异(调用者在开始时每个实体都有一个)以及如何使用这些差异。 2. 通过将实体特定代码委托给调用者,我们还使其更加灵活,这样我们就不必为每个实体特定逻辑创建一个 MethodToRefactor。

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

【问题讨论】:

    标签: c# generics expression-trees


    【解决方案1】:

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

    由于这两个表达式本质上是相同的,你应该只传递一个表达式和一个函数来从实体中获取键值。

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

    【讨论】:

    • 谢谢。我刚试了一下,var entity = entities.FirstOrDefault(filterExpression(refText)); 行似乎抛出“Method name expected”错误。不知道是什么原因造成的。
    • 这是因为给 FirstOrDefault 的方法没有好的签名。 FirstOrDefault 需要一个像这样的方法 Func,你正在给 Func。或者这可能是由于你的表达。任何人,如果我错了,请纠正我,我暂时还没有真正的表达经验。
    • entities.FirstOrDefault(e => filterExpression(e, refText))
    • 我已更新示例以使用返回 ExpressionFunc 以确保 IQueryable FirstOrDefault 正常工作。
    【解决方案2】:

    你可以这样做。

    给定一个T 类型,我们只需要一个string 属性的访问器来与refText 进行比较,还需要一个int 属性的访问器来比较keyValue。第一个用Expression&lt;Func&lt;T, string&gt;&gt; nameSelector表示,第二个用Expression&lt;Func&lt;T, int&gt;&gt; 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? 转换需要在 refText 不存在时允许返回 null

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

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

    客户:

    id = MethodToRefactor(db.Customer, e => e.CustomerName, e => e.CustomerId, ...);
    

    【讨论】:

      【解决方案3】:

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

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

      有了一个接口,你就可以抽象出它的关键属性了,

      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 属性,在我看来这并不是一件坏事。显然,您无论如何也将您的密钥属性用于其他东西(否则为什么此方法只返回一个密钥)。

      另一方面,当找到匹配项时,您正在检索整个实体,但您只关心密钥。这可以通过仅检索密钥来改善,例如在表达式的末尾添加一个选择。不幸的是,在这种情况下,您需要更多“魔法”才能让 ef(或您的 linq 提供者)理解 .Select(e => e.Key) 表达式(至少 ef 不会开箱即用)。由于我希望您在“...某些条件...”中需要整个实体,因此我在此答案中不包括此版本(也保持简短: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}, ...);
                          }
              }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-07-10
        • 2023-03-05
        • 2012-02-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-08-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多