使用委托解决常见编程问题






4.88/5 (16投票s)
本文展示了我们如何借助委托轻松解决各种技术问题。
引言
在日常使用最新 .Net 框架(3.5 及更高版本)进行编程时,我们或多或少地广泛使用委托。它可能以事件的形式出现,或者与 LINQ、任务/线程、MVC Razor 助手等一起使用,不一而足。自从引入 Lambda 表达式和 LINQ 以来,委托对我们来说变得如此自然。
我们大多数人通过为框架方法提供匿名方法实现来使用委托,但当涉及到我们自己的业务逻辑或某些代码时,委托根本不会出现在我们的脑海中。许多人只为事件使用委托,但通过观察基类库,我们可以很容易地看出委托是多么有用!本文展示了我们可以使用委托解决哪些不同的问题,以及它们如何简化我们的生活。所以,下次当你准备解决一些问题时,也考虑一下委托:)
背景
我假设您已经知道什么是委托和 Lambda 表达式,它们之间如何关联,框架提供了哪些不同的内置委托以及如何使用它们。如果不知道,这里有一个很好的链接可以学习。这里还有另一个链接,它也讨论了匿名函数和事件。
使用代码
1. 使用委托避免类库依赖
假设您有 3 个类库项目,分别是 Repository
、Common
和 Service
。顾名思义,Repository
旨在包含各种存储库,其中包含数据访问逻辑。同样,Service
是一个旨在包含业务逻辑的项目,而 Common
项目用于在您的解决方案中包含一些通用逻辑。
假设有一个需求,您需要一个依赖于某个 Repository
的通用逻辑,但您不希望您的 Common
程序集对其有依赖。在这种情况下,人们通常通过创建帮助服务或在 Service
程序集本身中引入另一个抽象来解决。但这种解决方案有一个“可争议的”缺点,即任何其他需要此逻辑的程序集都必须使用服务抽象,否则就不需要。这是可争议的,因为一旦您对 Repo 有了依赖,该逻辑很可能更适合 Service
程序集而不是 Common
程序集,并且该解决方案遵循了正确的设计原则/指南。争议的另一方面是“可重用性”,这是我们的解决方案在这里所缺乏的。有时,现实世界的业务需求非常复杂,以至于与设计原则相悖。争论还在继续,但撇开这些(因为它超出了本文的范围),我将专注于委托实现部分。
如果你对这一切感到困惑,别担心!一旦你看了代码示例,你就会明白。这是我正在谈论的代码片段
Repository
程序集 -
// A class in Repo Assembly
class Repo
{
public int GetValue()
{
return 100;
}
}
Common
程序集 -
// A class in Common Assembly
class Common
{
// TODO: Avoid the dependecy on Repo
static Repo repo = new Repo();
public static int OperateOnRepoValue()
{
// Operates on the value returned by the repository
return repo.GetValue() * 2;
}
}
Service
程序集 -
// A class in Service Assembly
class Service
{
// Already have a dependency on Repo and we are happy with it
Repo repo = new Repo();
public void Operate()
{
int result = Common.OperateOnRepoValue();
}
}
如上述代码所示,Common
类正在使用 Repo
的 GetValue
方法,从而创建对 Repository
程序集的依赖。如前所述,解决此问题的一种方法是在服务层引入抽象,或者引入一个新服务,仅仅是为了以可重用方式操作 Repository
值。然而,我将不提供此代码示例,因为它超出了本文的范围;相反,我将展示如何使用委托解决此问题。
使用 Function delegate
的解决方案 -
// A class in Common Assembly
class Common
{
// Takes a function delegate, which returns an integer
public static int OperateOnRepoValue(Func<int> func)
{
return func() * 2; // Multiply 2 into the return value of func and return the result
}
}
上述代码通过接受函数委托作为参数来移除对 Repository
的依赖。现在,调用者可以按照自己的意愿为该委托提供实现。
这是我们更新后的 Service
类 -
// A class in Service Assembly class Service { Repo repo = new Repo(); public void Operate() { // Pass an anonymous function which returns a value from the Repo int result = Common.OperateOnRepoValue(() => repo.GetValue()); } }
2. 使用 StopWatch
测量各种函数/方法执行(带有委托的“Using”模式)
有时,我们需要测量函数/方法的执行时间,建议您始终使用 StopWatch
以获得最大精度。如果我们需要测量多个函数,我们总是会为测试中的函数包装多个 StopWatch。这是一个例子
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
myMethod_1();
stopWatch.Stop();
Console.WriteLine(string.Format("{0}-Elapsed: {1}", "My Method 1", stopWatch.Elapsed));
stopWatch.Restart();
myMethod_2();
stopWatch.Stop();
Console.WriteLine(string.Format("{0}-Elapsed: {1}", "My Method 2", stopWatch.Elapsed));
我们可以避免这种混乱的代码,并始终以这种方式重用测量逻辑 -
public static void Measure(Action action, string actionName)
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
action();
stopWatch.Stop();
Console.WriteLine(string.Format("{0}-Elapsed: {1}", actionName, stopWatch.Elapsed));
}
上述方法接受一个 Action
委托并将其包装在 StopWatch
下。您可以这样使用它
static void Main(string[] args)
{
Measure(() => MyMethod_1(), "My method 1");
// Similarly, measure other methods
Console.ReadKey();
}
public static void MyMethod_1()
{
// Sleep for 2 seconds
Thread.Sleep(1000 * 2);
}
如果你的被测函数有返回值,请改用泛型 function delegate
。这是代码示例 -
public static T Measure<T>(Func<T> func, string actionName)
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
T result = func();
stopWatch.Stop();
Console.WriteLine(string.Format("{0}-Elapsed: {1}", actionName, stopWatch.Elapsed));
return result;
}
现在在 main 方法中 -
var result = Measure(() => MyMethod_1(), "My method 1");
3. 使用委托的工厂模式(非“工厂方法”)
听起来很疯狂?!.....其实不然,一旦你把它放在适当的位置,它看起来很自然。
注意:当我说工厂时,我指的是简单的对象创建,不要与涉及继承的工厂方法模式混淆。
假设您有一个 Asp.Net MVC 应用程序,其控制器中有一个接受参数的 Action 方法。根据此参数,您可能需要实例化不同的 Services
。您可能正在使用依赖容器来解析服务(或任何)依赖项。如果依赖项在编译时已知,那么一切都好,并且可以在应用程序启动/配置文件中轻松注册依赖项。但在我们的情况下,解析应该在运行时根据提供的参数发生,但您可能不希望将依赖项解析分散到各处。您可以使用委托轻松解决此问题。以下是步骤 -
创建类似这样的类
public static class ServiceFactory
{
// The Create property returns a generic function delegate, which accepts MyOptions enum
// and returns an instance of IService implementation.
public static Func<MyOptions, IService> Create { get; set; }
}
其中,MyOptions 是一个枚举
enum MyOptions
{
Option1,
Option2,
Option3
}
现在在您的应用程序启动时,在引导/组件注册方法中,您可以像这样解析依赖项
ServiceFactory.Create = (myOptions) =>
{
// Resolve dependencies based on the options
switch (myOptions)
{
case MyOptions.Option1: return container.Resolve<Service1>();
case MyOptions.Option2: return container.Resolve<Service2>();
case MyOptions.Option3: return container.Resolve<Service3>();
default: return container.Resolve<Service1>();
}
};
因此,在 action 方法中,您只需依赖工厂来解析依赖项
public ActionResult Index(string id, MyOptions myOptions)
{
IService service = ServiceFactory.Create(myOptions);
}
4. 在运行时更改属性/方法的行为(带有委托的“Using”模式)
假设您正在使用 实体框架
进行数据访问,并且您创建了通用存储库来处理实体。现在由于某种原因(通常是性能),您希望使用 .Net 框架的并行执行能力同时调度多个存储库调用。不幸的是,通用存储库模式不会开箱即用地允许您这样做,因为 DB Context
只允许一次执行一个线程。在这种情况下,显而易见的解决方案是在其自己的 新 DB Context
中创建 每个 repo 实例
。
在您的服务类中,您可能有很多方法,我们不确定何时哪些方法会并行执行。此外,参与并行执行的方法可能会随着业务需求和性能度量而改变。在这种情况下,您总是可以借助委托并以更可维护的方式解决它。以下是代码示例 -
public abstract class BaseService<Entity> where Entity: class
{
// Flag to switch the mode
private bool isConcurrencyEnabled = false;
// The repository instance to re-use
private IRepository<Entity> _repo = null;
protected IRepository<Entity> Repo
{
get
{
if (this.isConcurrencyEnabled)
{
// Return new instance of the repository
return RepoFactory.GetRepo<Entity>();
}
else
{
// Re-use existing repository
return this._repo;
}
}
}
public BaseService(IDbContext dbContext)
{
// Initialize repository
this._repo = dbContext.GetRepository<Entity>();
}
protected T ExecuteInParallel<T>(Func<T> action)
{
// Use try/finally pattern to leave the class always in a proper state
try
{
// Enable the flag before executing the action
this.isConcurrencyEnabled = true;
// Execute the action and return the result
return action();
}
finally
{
// Always disable the flag, before leaving the function
this.isConcurrencyEnabled = false;
}
}
}
具体的服务类可以这样使用它 -
public class MyService: BaseService<MyData>
{
public MyService(IDbContext dbContext)
: base(dbContext)
{ }
public List<MyData> GetMyData(List<int> ids)
{
if(ids == null)
{
throw new ArgumentNullException("The ids cannot be null");
}
// We need to restrict number of elements to balance the SQL connection pool
if(ids.Count > 5)
{
throw new ArgumentException("The number of elements should not exceed 5");
}
return this.ExecuteInParallel(() =>
{
ConcurrentBag<MyData> myData = new ConcurrentBag<MyData>();
// The context used in Repo of GetMyDataById will be different for each id
ids.AsParallel().ForAll(id => myData.Add(this.GetMyDataById(id)));
return myData.ToList();
});
}
public MyData GetMyDataById(int id)
{
return this.Repo.Find(x => x.Id == id).FirstOrDefault();
}
}
MyService
的消费者可以多次调用 GetMyDataById
,逐个传入不同的 ID(这会重用上下文),或者通过提供 ID 列表来使用 GetMyData
,该列表会并行执行(为每次 repo 调用创建新的上下文)。这个特定的例子意义不大,但这只是为了说明我们如何使用委托实现 Using
模式。实际上,它可以更大程度地实现,并且非常强大!我可能会写一篇关于这个主题的专门文章。
注意:为了简洁起见,上述代码未处理异常,但在实际项目中您必须处理它。
关注点
在这里我们看到了如何使用委托解决各种问题。同时,它也提醒您,这些并非开箱即用的解决方案。过度使用委托会使您的代码更难调试和理解。您需要深思熟虑地应用它们。这里需要注意的关键是,我们将委托应用于代码中“变化”的部分。如果您记住这一点,您就会简单地知道何时何地应用它。
历史
2015年8月24日 - 初版