使用“ using”关键字时如何实现DRY原理?


23

考虑以下方法:

public List<Employee> GetAllEmployees()
{
    using (Entities entities = new Entities())
    {
        return entities.Employees.ToList();
    }
}

public List<Job> GetAllJobs()
{
    using (Entities entities = new Entities())
    {
        return entities.Jobs.ToList();
    }
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    using (Entities entities = new Entities())
    {
        return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

使用块是相同的,这里已经重复了3次(当然,在实际应用中超过100次)。如何实现块的DRY(不重复自己)主体using?是否完全违反了DRY原则?

更新:我不是在谈论已在using块内实现的内容。我实际上在这里是指using (Entities entities = new Entities())。该行重复100次以上。


2
这是C#吗?您问题的答案可能取决于语言
大卫,

是的@David,很抱歉我没有提到我的语言。它如何影响答案?
2011年

某些语言可能具有特定的语法,可以帮助您分解一些代码。我不了解C#,但是在Ruby中,我认为您可以使用块来分解使用部分。
大卫,

using语句实际上提供C#语言支持DRY原则适用于帮助避免重复,同时管理资源配置与编码的Dispose设计模式。这并不意味着我们无法找到使DRYer变干的方法!我个人认为DRY是一个递归过程。
约翰·托伯勒

Answers:


24

一种想法是将其包装为带有的函数Func

像这样

public K UsingT<T,K>(Func<T,K> f) where T:IDisposable,new()
{
    using (T t = new T())
    {
        return f(t);
    }
}

然后你上面的代码变成

public List<Employee> GetAllEmployees()
{
    return UsingT<Entities,List<Employee>>(e=>e.Employees.ToList());
}

public List<Job> GetAllJobs()
{
    return UsingT<Entities,List<Job>>(e=>e.Jobs.ToList());
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return UsingT<Entities,List<Task>>(e=>e.Tasks.Where(t => t.JobId == job.Id).ToList());
}

我也做Entities了类型参数,因为我假设您要使用多个类型。如果不是,则可以将其删除,而只需将type param用作返回类型。

老实说,尽管这种代码根本无法提高可读性。以我的经验,更多的Jr同事也很难过。

更新 您可能会考虑的一些关于助手的其他变化

//forget the Entities type param
public T UsingEntities<T>(Func<Entities,T> f)
{
    using (Entities e = new Entities())
    {
        return f(e);
    }
}
//forget the Entities type param, and return an IList
public IList<T> ListFromEntities<T>(Func<Entities,IEnumerable<T>> f)
{
    using (Entities e = new Entities())
    {
        return f(e).ToList();
    }
}
//doing the .ToList() forces the results to enumerate before `e` gets disposed.

1
+1不错的解决方案,尽管它没有解决实际的问题(原始问题未包括在内),即出现了Entities
back2dos

@Doc Brown:我想我用函数名称击败了它。IEnumerable万一IEnumerable调用者想要返回的T的任何非属性,我就忽略了该函数,但是您是对的,它将对其进行清理。也许同时为Single和IEnumerable结果提供帮助会很好。就是说,我仍然认为这会减慢对代码功能的认识,特别是对于不习惯使用大量泛型和lambda的人(例如,您的同事并不熟悉:))
Brook,

+1我认为这种方法很好。并且可以提高可读性。例如,将“ ToList”放入WithEntities,使用Func<T,IEnumerable<K>>代替Func<T,K>,并为“ WithEntities”赋予更好的名称(例如SelectEntities)。而且我认为“实体”不需要在这里成为通用参数。
布朗

1
为了使这项工作,约束将需要where T : IDisposable, new(),如using需要IDisposable以工作。
安东尼·佩格拉姆

1
@安东尼·佩格拉姆:固定,谢谢!(这就是我在浏览器窗口中编码C#所得到的东西)
Brook,

23

对我来说,这就像担心多次遍历同一集合:这只是您需要做的事情。任何进一步对其进行抽象的尝试都会使代码的可读性大大降低。


打个比方@Ben。+1
Saeed Neamati 2011年

3
我不同意,对不起。阅读OP关于事务范围的注释,并考虑编写500次此类代码后必须执行的操作,然后注意,您必须将同一件事更改500次。仅当您具有不到10个此类功能时,这种代码重复才可以。
布朗

哦,别忘了,如果您以非常相似的方式对同一个集合进行for-for十次以上,并且for-each中包含看起来相似的代码,那么您应该明确地考虑一些概括。
布朗

1
对我来说,好像您是在将三个衬套制成一个衬套……您仍在重复自己。
Jim Wolff 2012年

例如,这是取决于上下文的,例如,如果该对象foreach是一个非常大的集合,或者该foreach循环内的逻辑很耗时。我的座右铭是:别
着迷

9

听起来您好像将“一次且只有一次”原理与DRY原理混淆了。DRY原则指出:

每条知识都必须在系统中具有单一,明确,权威的表示形式。

但是,一次和唯一一次原理稍有不同。

[DRY]原理类似于OnceAndOnlyOnce,但目标不同。鼓励您使用OnceAndOnlyOnce进行重构,以消除重复的代码和功能。使用DRY,您尝试确定系统中使用的每条知识的唯一权威来源,然后使用该来源生成该知识的适用实例(代码,文档,测试等)。

DRY原理通常在实际逻辑的上下文中使用,而使用using语句并没有那么多冗余:

保持程序DRY的结构更困难,价值更低。业务规则,if语句,数学公式和元数据应仅出现在一个位置。WET内容-HTML页面,冗余测试数据,逗号和{}分隔符-都很容易被忽略,因此将它们干燥就显得不那么重要了。

资源


7

我看不到using这里的使用:

怎么样:

public List<Employee> GetAllEmployees() {
    return (new Entities()).Employees.ToList();
}
public List<Job> GetAllJobs() {
    return (new Entities()).Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return (new Entities()).Tasks.Where(t => t.JobId == job.Id).ToList();
}

甚至更好,因为我认为您不必每次都创建一个新对象。

private Entities entities = new Entities();//not sure C# allows for that kind of initialization, but you can do it in the constructor if needed

public List<Employee> GetAllEmployees() {
    return entities.Employees.ToList();
}
public List<Job> GetAllJobs() {
    return entities.Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

至于违反DRY:DRY不适用于此级别。实际上,除了可读性之外,没有任何原理可以实现。尝试在该级别上应用DRY实际上只是架构上的微优化,就像所有微优化一样,它只是丢车而已,并没有解决任何问题,甚至存在引入新问题的风险。
根据我自己的经验,我知道如果您尝试在该级别上减少代码冗余,则会混淆真正清晰和简单的内容,从而对代码质量产生负面影响。

编辑:
好的。因此,问题实际上不是using语句,而是问题是每次都依赖于您创建的对象。我建议注入一个构造函数:

private delegate Entities query();//this should be injected from the outside (upon construction for example)
public List<Employee> GetAllEmployees() {
    using (var entities = query()) {//AFAIK C# can infer the type here
        return entities.Employees.ToList();
    }
}
//... and so on

2
但是@ back2dos,在很多地方我们using (CustomTransaction transaction = new CustomTransaction())在代码中使用代码块来定义交易的范围。不能将其捆绑为一个对象,并且在每个要使用事务的地方,都应该编写一个块。现在,如果你想要的东西,从改变交易的类型CustomTransaction,以BuiltInTransaction超过500点的方法呢?在我看来,这是重复的工作,并且是违反DRY原则的例子。
2011年

3
在这里使用“使用”会关闭数据上下文,因此在这些方法之外不可能进行延迟加载。
史蒂文·斯特里加

@Saeed:那是您研究依赖项注入的时候。但这似乎与问题中所述的情况大不相同。
的CVn

@Saeed:帖子已更新。
back2dos

@WeekendWarrior using(在这种情况下)是否还是一个更未知的“便捷语法”?为什么这么好用=)
Coops 2013年

4

不仅使用重复代码(顺便说一句,它是重复代码,实际上与try..catch..finally语句进行比较),而且toList也是如此。我会像这样重构您的代码:

 public List<T> GetAll(Func<Entities, IEnumerable<T>> getter) {
    using (Entities entities = new Entities())
    {
        return getter().ToList();
    }
 }

public List<Employee> GetAllEmployees()
{
    return GetAll(e => e.Employees);
}

public List<Job> GetAllJobs()
{
    return GetAll(e => e.Jobs);
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return GetAll(e => e.Tasks.Where(t => t.JobId == job.Id));
}

3

由于这里除了最后一个之外,没有任何形式的业务逻辑。在我看来,它不是真正的干燥。

最后一个在using块中没有任何DRY,但我猜where子句应该在使用过的任何地方更改。

这是代码生成器的典型工作。编写并覆盖代码生成器,并为每种类型生成代码。


不@arunmur。这里有一个误会。DRY,我的意思是using (Entities entities = new Entities())封锁。我的意思是,这行代码重复了100次,并且越来越重复。
2011年

DRY主要来自以下事实:您需要编写多次测试用例,并且一个地方的bug意味着您必须在100个地方修复它。using(Entities ...)太简单,无法破解。它不需要拆分或放在另一个类中。如果您仍然坚持简化它。我建议从ruby的Yeild回调函数。
2011年

1
@arnumur-使用并不总是那么容易打破。我经常有很多逻辑来决定要在数据上下文中使用哪些选项。这是非常有可能的是,错误的连接字符串可以通过英寸
摩根Herlocker

1
@ironcode-在这种情况下,将其推到某个函数是有意义的。但在这些示例中,很难破解。即使确实无法解决问题,通常也应该在Entities类本身的定义中进行修复,这应该用一个单独的测试用例进行介绍。
2011年

+1 @arunmur-我同意。我通常自己做。我为该函数编写了测试,但为您的using语句编写测试似乎有点多余。
Morgan Herlocker 2011年

2

由于您要一遍又一遍地创建和销毁同一个一次性对象,因此,您的类本身就是实现IDisposable模式的一个不错的选择。

class ThisClass : IDisposable
{
    protected virtual Entities Context { get; set; }

    protected virtual void Dispose( bool disposing )
    {
        if ( disposing && Context != null )
            Context.Dispose();
    }

    public void Dispose()
    {
        Dispose( true );
    }

    public ThisClass()
    {
        Context = new Entities();
    }

    public List<Employee> GetAllEmployees()
    {
        return Context.Employees.ToList();
    }

    public List<Job> GetAllJobs()
    {
        return Context.Jobs.ToList();
    }

    public List<Task> GetAllTasksOfTheJob(Job job)
    {
        return Context.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

在创建类的实例时,只需要“使用”即可。如果您不希望类负责处理对象,则可以使方法接受依赖项作为参数:

public static List<Employee> GetAllEmployees( Entities entities )
{
    return entities.Employees.ToList();
}

public static List<Job> GetAllJobs( Entities entities )
{
    return entities.Jobs.ToList();
}

public static List<Task> GetAllTasksOfTheJob( Entities entities, Job job )
{
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

1

我最喜欢的无与伦比的魔法!

public class Blah
{
  IEnumerable<T> Wrap(Func<Entities, IEnumerable<T>> act)
  {
    using(var entities = new Entities()) { return act(entities); }
  }

  public List<Employee> GetAllEmployees()
  {
    return Wrap(e => e.Employees.ToList());
  }

  public List<Job> GetAllJobs()
  {
    return Wrap(e => e.Jobs.ToList());
  }

  public List<Task> GetAllTasksOfTheJob(Job job)
  {
    return Wrap(e => e.Tasks.Where(x ....).ToList());
  }
}

Wrap存在只是为了将其抽象出来或您需要的任何魔法。我不确定我会一直建议这样做,但是可以使用。“更好”的想法是使用像StructureMap这样的DI容器,并将Entities类的作用域限定在请求上下文中,将其注入到控制器中,然后让它处理生命周期,而无需您的控制器。


您对Func <IEnumerable <T>,Entities>的类型参数的顺序错误。(请参阅我的回答,它基本上是一样的)
布鲁克

好吧,很容易纠正。我不记得正确的顺序是什么。Func我应该用够了。
特拉维斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.