背景
这是一个重构问题。我有一堆方法或多或少具有完全相同的代码,但它们作用于不同的类型。每种类型基本上有一种方法,我想将它们全部组合成一个可以使用泛型类型的方法。
当前代码
以下代码可能有助于解释我正在尝试的内容 -
以下方法主要在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#语言功能实现这些目标的解决方案。这并不意味着我是反经典设计模式,只是我不喜欢创建一堆新类的想法,因为我可以通过重构几种方法来创建它。
答案 0 :(得分:2)
如果实体没有相同的基类,那么你能做的最好就是有一个类约束。
由于两个表达式基本相同,因此您应该只传递一个表达式和一个函数来从实体中获取键值。
Count
和First
方法也可以合并为一个语句,然后检查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}, ...);
}
}
}