65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (16投票s)

2015 年 8 月 25 日

CPOL

7分钟阅读

viewsIcon

23542

downloadIcon

185

本文展示了我们如何借助委托轻松解决各种技术问题。

引言

在日常使用最新 .Net 框架(3.5 及更高版本)进行编程时,我们或多或少地广泛使用委托。它可能以事件的形式出现,或者与 LINQ、任务/线程、MVC Razor 助手等一起使用,不一而足。自从引入 Lambda 表达式和 LINQ 以来,委托对我们来说变得如此自然。

我们大多数人通过为框架方法提供匿名方法实现来使用委托,但当涉及到我们自己的业务逻辑或某些代码时,委托根本不会出现在我们的脑海中。许多人只为事件使用委托,但通过观察基类库,我们可以很容易地看出委托是多么有用!本文展示了我们可以使用委托解决哪些不同的问题,以及它们如何简化我们的生活。所以,下次当你准备解决一些问题时,也考虑一下委托:)

背景

我假设您已经知道什么是委托和 Lambda 表达式,它们之间如何关联,框架提供了哪些不同的内置委托以及如何使用它们。如果不知道,这里有一个很好的链接可以学习。这里还有另一个链接,它也讨论了匿名函数和事件。

使用代码

1. 使用委托避免类库依赖

假设您有 3 个类库项目,分别是 RepositoryCommonService。顾名思义,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 类正在使用 RepoGetValue 方法,从而创建对 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日 - 初版

© . All rights reserved.