在练习中的模式

对于日常的.NET 开发的函数式编程

Jeremy Miller

在过去的三个或四年中,什么是.NET 的生态系统中最重要前进量? 您可以尝试新的技术或框架,如 Windows 通信基础 (WCF) 或 Windows 演示基础 (WPF) 的名称。 为我个人,但是,我会说.NET 框架的该功能强大,最后两个版本通过 C# 和 Visual Basic 语言添加我日常开发活动上有更严重影响。 在本文中我想特别检查新的支持的功能的编程技术,.NET 3.5 中可以如何帮助您执行以下:

  1. 使代码更多的声明性。
  2. 减少代码错误。
  3. 编写更少的许多常见任务的代码行。

在所有的许多 incarnations 中语言集成查询 (LINQ) 功能是在.NET 中,函数式编程的明显且功能强大使用,但这只是提示的在 iceberg。

要保持的"日常开发"主题我已将我的代码示例的大多数基于 sprinkled 中某些 JavaScript 与 C# 3.0。 请注意某些其他,较新编程语言的 CLR,如 IronPython、 IronRuby 和 F # 具有显著增强的支持或更有用的该功能的编程技术,本文所示的语法。 遗憾的是,VBA 的当前版本不支持多行 Lambda 函数,因此许多种技术显示在此处不为使用 Visual Basic 中。 但是,我将催促考虑在下一版本的语言,准备这些技术的 Visual Basic 开发人员在 Visual Studio 2010 中传送。

一流的函数

函数式编程的某些元素有可能在 C# 中或 Visual Basic 因为我们现在一流的函数可以围绕方法,之间传递的所以保存为变量甚至从另一种方法返回。 从.NET 2.0 和较新的.NET 3.5 Lambda 表达式的匿名委托是 C# 和 Visual Basic 实现但"Lambda 表达式"的一类函数的方式表示计算机科学中更具体的内容。 一类函数的另一个常见术语是"阻止。本文的其余部分,我将使用术语"阻止"表示一类的函数而不是"关闭"(一种特定类型的一流我在下一步介绍的函数) 或"Lambda,"若要避免意外的错误 (和该 wrath 的实际功能的编程专家)。 闭包包含在关闭函数之外定义的变量。 如果您已经使用 JavaScript 开发越来越流行 jQuery 库,您可能已经非常频繁地使用关闭事件。 以下是使用来自我的当前项目的一个闭合的一个示例:

// Expand/contract body functionality          
var expandLink = $(".expandLink", itemElement);
var hideLink = $(".hideLink", itemElement);
var body = $(".expandBody", itemElement);
body.hide();

// The click handler for expandLink will use the
// body, expandLink, and hideLink variables even
// though the declaring function will have long
// since gone out of scope.
expandLink.click(function() {
    body.toggle();
    expandLink.toggle();
    hideLink.toggle();
});

此代码用于设置显示或隐藏内容,我们在 Web 页上的,通过单击一个 < 一个 > 一个非常典型 accordion 效果元素。 通过使用在关闭外创建的变量的闭包函数中传递,我们定义单击处理程序的该 expandLink。 可以单击该 expandLink 用户,但单击处理程序将仍然能够使用正文和 hideLink 变量之前多长时间,同时包含变量和单击处理程序函数将退出。

为数据 lambdas

在某些的情况中使用可以 Lambda 语法表示可以被用作数据而执行的代码中的表达式。 我特别不理解多次读取它的语句第一,因此让我们看一个 Lambda 视为数据从使用 Fluent NHibernate 库的显式对象/关系映射的一个示例:

 

public class AddressMap : DomainMap<Address>
    {
        public AddressMap()
        {
            Map(a => a.Address1);
            Map(a => a.Address2);
            Map(a => a.AddressType);
            Map(a => a.City);
            Map(a => a.TimeZone);
            Map(a => a.StateOrProvince);
            Map(a => a.Country);
            Map(a => a.PostalCode);
        }
    }

fluent NHibernate 永远不会计算表达式,一个 = >a.Address1。 而是,它分析表达式以查找在基础 NHibernate 映射中使用的地址 1 的名称。 此方法已通过.NET 空间中的多个最近打开的源项目广泛传播。 使用 Lambda 表达式只在 PropertyInfo 对象和属性名称通常称为静态反射。

传递限制

研究函数式编程将最佳原因之一是要了解如何一流的功能使您可以通过提供一种更精细机制,用于组合比类减少在代码中的重复。 通常,您将会遇到以外的某个位置的中间序列的一步其基本窗体实质上是相同的代码序列。 面向对象编程,您可以使用继承使用模板方法模式以消除重复尝试。 越来越多,我发现传递表示该变量的块中的步骤中间实现基本序列为清洗器可以消除重复的另一个方法。

若要使 API,易于使用和不太容易出错,最佳方法之一是减少重复代码。 例如,考虑 API 用于访问远程服务或资源 (如 ADO.NET IDbConnection 对象或要求状态或持久连接的套接字侦听器的常见情况。 您必须通常"打开"在连接之前使用资源。 这些状态的连接通常是昂贵或不足的资源,因此通常将"关闭"一旦将释放该资源为其他进程或线程完成连接。

下面的代码演示一个具有代表性的界面,网关到某些类型的连接状态:

 

public interface IConnectedResource
    {
        void Open();
        void Close();

        // Some methods that manipulate the connected resource
        BigDataOfSomeKind FetchData(RequestObject request);
        void Update(UpdateObject update);
    }

另一个类使用此 IConnectedResource 接口的每一次 Open 方法调用使用其他任何方法之前进行 Close 方法应始终调用之后,如图 1 的中所示。

早期文章中我在我们的设计中所述与仪式本质的概念。 (请参阅 msdn.microsoft.com/magazine/dd419655.aspx)。"本质"在 ConnectedSystemConsumer 的类的职责就是用于连接的资源更新一些信息。 遗憾的是的大部分代码在 ConnectedSystemConsumer 关心"仪式"连接和断开该 IConnectedResource 接口和错误处理。

图 1 使用 IConnectedResource

 

public class ConnectedSystemConsumer
{
private readonly IConnectedResource _resource;
public ConnectedSystemConsumer(IConnectedResource resource)
{
_resource = resource;
}
public void ManipulateConnectedResource()
{
try
{
// First, you have to open a connection
_resource.Open();
// Now, manipulate the connected system
_resource.Update(buildUpdateMessage());
}
finally
{
_resource.Close();
}
}
}

更糟糕的是尚未是,"执行下列尝试/打开内容/最终/关闭"仪式代码具有要复制的 IConnectedResource 接口的每个使用中。 如我之前讨论的以提高您的设计,最佳方法之一是标记出重复,只要它 creeps 到您的代码。 让我们来尝试一种使用块或闭包 IConnectedResource API 不同的方法。 第一次,我将应用该接口隔离原则 (请参阅有关详细信息的 objectmentor.com/resources/articles/isp.pdf) 提取严格地打开或关闭调用方法而该连接的资源的接口:

 

public interface IResourceInvocation
    {
        BigDataOfSomeKind FetchData(RequestObject request);
        void Update(UpdateObject update);
    }

下一步中,我创建第二个接口,用于严格连接表示的资源访问由 IResourceInvocation 接口:

 

public interface IResource
    {
        void Invoke(Action<IResourceInvocation> action);
    }

现在,让我们来重写 ConnectedSystemConsumer 类以使用较新的、 功能样式 API:

 

public class ConnectedSystemConsumer
    {
        private readonly IResource _resource;
 
        public ConnectedSystemConsumer(IResource resource)
        {
            _resource = resource;
        }

        public void ManipulateConnectedResource()
        {
            _resource.Invoke(x =>
            {
                x.Update(buildUpdateMessage());
            });
        }
    }

关心如何设置或消除连接资源下将不再具有 ConnectedSystemConsumer 此新版本。 在起的方式作用 ConnectedSystemConsumer 只告诉 IResource 界面,以"转最多到在第一个 IResourceInvocation 您查看并为其提供这些说明"通过 IResource.Invoke 方法块或闭包中传递。 所有的重复"执行下列尝试/打开内容/最后/关闭"我已前抱怨的仪式代码现在处于 IResource 的具体实现的图 2 所示。

图 2 具体实现的 IResource

 

frepublic
class Resource : IResource
{
public void Invoke(Action<IResourceInvocation> action)
{
IResourceInvocation invocation = null;
try
{
invocation = open();
// Perform the requested action
action(invocation);
}
finally
{
close(invocation);
}
}
private void close(IResourceInvocation invocation)
{
// close and teardown the invocation object
}
private IResourceInvocation open()
{
// acquire or open the connection
}
}

我会认为我们已通过打开和关闭连接到外部资源到资源类负责改进我们的设计和 API 可用性。 我们也已经提高代码的结构封装核心工作流应用程序的基础结构问题的详细信息。 ConnectedSystemConsumer 的第二个版本知道最少的外部连接资源在工作原理比第一个版本。 第二个设计使您能够更轻松地更改您的系统如何与外部连接资源而无需更改和可能不稳定系统的核心工作流代码。

第二个设计还使您的系统更少出错-通过消除重复的"重试/打开/最终/关闭"循环。 每次开发人员必须重复该代码,他风险进行编码的错误的可能从技术上讲正常工作,但消耗资源并损坏应用程序的可伸缩性特征。

延迟的执行

若要了解有关函数式编程最重要概念之一就是延迟的执行。 幸运的是,这一概念也是相对简单的。 这意味着,块函数定义了所有不一定立即执行内联。 让我们看一下实际使用延迟执行。

在很大的 WPF 应用程序中我使用称为 IStartable 一个标记接口表示需要被,还,启动应用程序的启动过程的一部分的服务。

 

public interface IStartable
    {
        void Start();
    }

为此特定的应用程序的所有服务的注册并通过应用程序 (在此例 StructureMap) 的控制反转容器中。 在启动应用程序却要动态地发现需要启动的应用程序中的所有服务并启动它们的代码将下列位:

 

// Find all the possible services in the main application
// IoC Container that implements an "IStartable" interface
List<IStartable> startables = container.Model.PluginTypes
    .Where(p => p.IsStartable())
    .Select(x => x.ToStartable()).ToList();
         

// Tell each "IStartable" to Start()
startables.Each(x => x.Start());

有三个 Lambda 表达式,此代码中。 假设您附加到此代码的.NET 基类库的完整源代码副本,然后试图使用调试器逐句通过它。 褰撴偍灏濊瘯位置单步执行选择,或每个调用,您会注意到 Lambda 表达式不被执行代码的下一个行并且为这些方法循环访问 container.Model.PluginTypes 成员的内部结构,Lambda 表达式所有多次都执行。 另一种考虑延迟执行的方法,调用每个方法时,您您只需告诉每一种方法如何操作,只要它是通过 IStartable 对象。

Memoization

memoization 是用来避免通过重复使用相同的输入与以前执行的结果执行昂贵的函数调用的优化技术。 我首先附带到联系该术语 memoization 在 F # 的函数式编程方面,但在研究这篇文章的过程中我意识到我的工作组经常使用 memoization 中我们 C# 开发。 假设您经常需要检索给定区域与此类服务的计算的财务数据的某种:

 

public interface IFinancialDataService
    {
        FinancialData FetchData(string region);
    }

IFinancialDataService 正好是执行非常慢,财务数据相当静态,因此应用 memoization 将是非常有用的应用程序的响应。 您可以创建一个包装为显示在的图 3 中实现内部 IFinancialDataService 类 memoization IFinancialDataService 实现。

图 3 实现的内部 IFinancialDataService 类

 

public class MemoizedFinancialDataService : IFinancialDataService
{
private readonly Cache<string, FinancialData> _cache;
// Take in an "inner" IFinancialDataService object that actually
// fetches the financial data
public MemoizedFinancialDataService(IFinancialDataService
innerService)
{
_cache = new Cache<string, FinancialData>(region =>
innerService.FetchData(region));
}
public FinancialData FetchData(string region)
{
return _cache[region];
}
}

缓存 < TKey、 TValue >类本身是只需包装一个词典 < TKey、 TValue >对象。 图 4 显示了缓存类的一部分。

图 4 的缓存类

public class Cache<TKey, TValue> : IEnumerable<TValue> where TValue :
class
{
private readonly object _locker = new object();
private readonly IDictionary<TKey, TValue> _values;
private Func<TKey, TValue> _onMissing = delegate(TKey key)
{
string message = string.Format(
"Key '{0}' could not be found", key);
throw new KeyNotFoundException(message);
};
public Cache(Func<TKey, TValue> onMissing)
: this(new Dictionary<TKey, TValue>(), onMissing)
{
}
public Cache(IDictionary<TKey, TValue>
dictionary, Func<TKey, TValue>
onMissing)
: this(dictionary)
{
_onMissing = onMissing;
}
public TValue this[TKey key]
{
get
{
// Check first if the value for the requested key
// already exists
if (!_values.ContainsKey(key))
{
lock (_locker)
{
if (!_values.ContainsKey(key))
{
// If the value does not exist, use
// the Func<TKey, TValue> block
// specified in the constructor to
// fetch the value and put it into
// the underlying dictionary
TValue value = _onMissing(key);
_values.Add(key, value);
}
}
}
return _values[key];
}
}
}

如果您感兴趣的缓存类内部您可以在几个包括 StructureMap,StoryTeller,FubuMVC 的开源软件项目中查找其版本,并我认为,Fluent NHibernate。

减少映射/图案

事实证明与功能的编程技术的简单许多常见的开发任务。 特别,列表,在代码中的设置操作最简单 mechanically 中支持 “ 减少映射 / ” 模式的语言。 (在 LINQ,“ 映射 ” 是 “ 选择 ” “ 减少 ” 并且 “ 聚合 ”)。考虑如何将计算的一个整数数组的总和。 在.NET 1.1,必须循环访问数组如下所示:

int [] 数字 = 新的 int [] {1,2,3,4,5} ;
int 总和 = 0 ;
为 (int i = 0 ;我 <numbers.length ;i + +)
{
总和 + = 数字 [i] ;
}

Console.WriteLine(sum) ;

波形的.NET 3.5 中支持 LINQ 的语言增强功能提供常用功能的编程语言中减少映射/功能。 目前,上述代码只是可以写成:

int [] 数字 = 新的 int [] {1,2,3,4,5} ;
int 总和 = numbers.aggregate ((x, y) = >x + y) ;

或更简单:

int 总和 = numbers.Sum() ;
Console.WriteLine(sum) ;

继续

大致放入延续编程中的是"下一步操作什么"表示某种类型的抽象或者"的其余部分计算。"有时,是很有价值,在为在向导的应用程序的用户可以明确地允许下一步或取消整个过程中的其他时间完成计算过程的一部分。

让我们跳到一个代码示例的右侧。 假设您正在开发 WinForms WPF 中的一个桌面应用程序。 您经常需要启动某些类型的长时间运行进程或从一个屏幕操作中访问慢速外部服务。 可用性,您肯定不希望锁定用户界面和,以使它响应服务调用发生时, 在后台线程中运行它。 服务调用最后不返回时, 可能要用服务,回来自数据更新用户界面,但为任何有经验的 WinForms 或 WPF 开发人员知道,您可以更新仅在主用户界面线程上的用户界面元素。

可以肯定使用 BackgroundWorker 在 System.ComponentModel 命名空间中的类,但我更喜欢根据传递 Lambda 表达式,此接口表示一个 CommandExecutor 对象对象到一个不同的方法:

public interface ICommandExecutor
    {
        // Execute an operation on a background thread that
        // does not update the user interface
        void Execute(Action action);

        // Execute an operation on a background thread and
        // update the user interface using the returned Action
        void Execute(Func<Action> function);
    }

第一个方法是只需在后台线程中执行的活动语句。 第二个为 Func < 操作 > 中采用的方法是更有趣。 看看如何使用此方法将通常是应用程序代码中。

第一次,假定您正在构造 WinForms 或 WPF 代码与模型查看演示者模式的监护控制器窗体。 (请参阅有关详细信息的 msdn.microsoft.com/magazine/cc188690.aspx MVP 模式。在这种模型将表示器类负责为一个长时间运行的服务方法调用,并使用返回的数据更新视图。 新演示者类只需将使用显示来处理所有线程和线程封送处理工作,如 图 5 中所示的早期 ICommandExecutor 界面。

图 5 中的表示器类

public class Presenter
{
private readonly IView _view;
private readonly IService _service;
private readonly ICommandExecutor _executor;
public Presenter(IView view, IService service, ICommandExecutor
executor)
{
_view = view;
_service = service;
_executor = executor;
}
public void RefreshData()
{
_executor.Execute(() =>
{
var data = _service.FetchDataFromExtremelySlowServiceCall();
return () => _view.UpdateDisplay(data);
});
}
}

表示器类调用 ICommandExecutor.Execute 通过传递在返回另一个块的块。 原始块调用长时间运行服务调用获取某些的数据,并返回可用于更新用户界面 (鍦 ㄨ 繖绉嶆儏鍐典笅 IView) 的继续符块。 在该特定情况下很重要的只更新该 IView 在同一时间,因为更新具有封送回用户界面线程而不是使用继续符的方法。

图 6 显示 ICommandExecutor 接口的具体实现。

图 6 的 ICommandExecutor 具体实现

public class AsynchronousExecutor : ICommandExecutor
{
private readonly SynchronizationContext _synchronizationContext;
private readonly List<BackgroundWorker> _workers =
new List<BackgroundWorker>();
public AsynchronousExecutor(SynchronizationContext
synchronizationContext)
{
_synchronizationContext = synchronizationContext;
}
public void Execute(Action action)
{
// Exception handling is omitted, but this design
// does allow for centralized exception handling
ThreadPool.QueueUserWorkItem(o => action());
}
public void Execute(Func<Action> function)
{
ThreadPool.QueueUserWorkItem(o =>
{
Action continuation = function();
_synchronizationContext.Send(x => continuation(), null);
});
}
}

(Func < 操作 >) 的 Execute 方法调用 Func < 操作 >在后台线程和时间的继续符 (返回 Func < 操作 > 操作) 和使用一个 SynchronizationContext 对象到主用户界面线程中执行将继续符。

我喜欢在传递到 ICommandExecutor 界面因调用在后台处理所需的方式很少 ceremonial 代码的块。 在这种方法的一个早期 incarnation,我们 Lambda 表达式或匿名委托在 C# 中之前就一个类似的实现使用类似于以下的一些命令模式类:

public interface ICommand
    {
        ICommand Execute();
    }

    public interface JeremysOldCommandExecutor
    {
        void Execute(ICommand command);
    }

前一种方法的缺点是我必须编写其他命令类只是后台操作和查看更新代码模型。 额外的类声明和构造函数函数是我们可以消除使用最功能的方法的更多的仪式代码但我更重要的功能的方法允许将此密切相关的所有代码都放入表示器而不是不必通过这些小的命令类扩展它的一个位置。

继续传递样式

生成继续符概念上,可以使用由传递的而不是等待该方法的返回值的一个继续符在调用方法的继续符传递的编程样式。 该方法接受在继续符是负责决定是否以及何时调用将继续符。

我当前的 MVC Web 项目中有几十个控制器从来自客户端浏览器通过一个 AJAX 调用域实体对象的用户输入保存更新的操作。 大多数这些控制器操作只需调用保存更改的实体,知识库类,但其他操作执行持久性工作中使用其他服务。 (请参阅我在 4 月的 MSDN 杂志msdn.microsoft.com/magazine/dd569757.aspx 知识库类有关的信息在的文章。

这些控制器操作的基本工作流是一致的:

  1. 验证域实体,并记录的任何验证错误。
  2. 如果有验证错误,返回响应客户端,表明该操作失败,并包括用于在客户端上显示验证错误。
  3. 如果有,没有验证错误保持域实体并返回到客户端,该值指示操作已成功响应。

我们希望做什么是集中在基本工作流,但仍允许每个单独的控制器操作,如何实现实际的持久性的不同。 现在我的工作组这样通过继承一个 CrudController < T >使用大量的模板的方法的每个子类可以重写以便添加或更改基本行为的超类。 此策略也在第一个,以此,但它快速分解随着变体。 现在我的工作组要移到使用继续符传递样式代码具有委托给类似于下面的接口我们控制器操作:

 

public interface IPersistor
    {
        CrudReport Persist<T>(T target, Action<T> saveAction);
        CrudReport Persist<T>(T target);
    }

一个典型的控制器操作会告诉 IPersistor 执行基本的 CRUD 工作流,并提供一个继续符 IPersistor 用于实际保存目标对象。 图 7 显示调用 IPersistor,但比我们基本知识库使用一个不同的服务,实际的持久性的一个示例控制器操作。

图 7 中的示例控制器操作

public class SolutionController
{
private readonly IPersistor _persistor;
private readonly IWorkflowService _service;
public SolutionController(IPersistor persistor, IWorkflowService
service)
{
_persistor = persistor;
_service = service;
}
// UpdateSolutionViewModel is a data bag with the user
// input from the browser
public CrudReport Create(UpdateSolutionViewModel update)
{
var solution = new Solution();
// Push the data from the incoming
// user request into the new Solution
// object
update.ToSolution(solution);
// Persist the new Solution object, if it's valid
return _persistor.Persist(solution, x => _service.Create(x));
}
}

我认为重要此处需注意的是 IPersistor 本身决定是否以及何时将调用提供 SolutionController 将继续符。 图 8 显示了一个示例实现的 IPersistor。

图 8 的 IPersistor 的实现

public class Persistor : IPersistor
{
private readonly IValidator _validator;
private readonly IRepository _repository;
public Persistor(IValidator validator, IRepository repository)
{
_validator = validator;
_repository = repository;
}
public CrudReport Persist<T>(T target, Action<T> saveAction)
{
// Validate the "target" object and get a report of all
// validation errors
Notification notification = _validator.Validate(target);
// If the validation fails, do NOT save the object.
// Instead, return a CrudReport with the validation errors
// and the "success" flag set to false
if (!notification.IsValid())
{
return new CrudReport()
{
Notification = notification,
success = false
};
}
// Proceed to saving the target using the Continuation supplied
// by the client of IPersistor
saveAction(target);
// return a CrudReport denoting success
return new CrudReport()
{
success = true
};
}
public CrudReport Persist<T>(T target)
{
return Persist(target, x => _repository.Save(x));
}
}

编写更少的代码

坦白地说,因为我想了解更多有关函数式编程以及如何应用它即使在 C# 或 Visual Basic 中也我最初选择本主题。 在编写本文的过程中我已经学习了许多有关多么有用功能的编程技术可以在正常的日常任务。 我已经达到最重要的结论,什么我试着此处,传达,与其他技术函数式编程方法通常会导致编写更少的代码和一些任务通常更多声明性代码,而这几乎总是一件好事。

Jeremy MillerC# 的一个 Microsoft MVP 也是为使用.NET 的依赖关系注入打开源 StructureMap (structuremap.sourceforge.net) 工具和在.NET 中 supercharged FIT 测试即将推出 StoryTeller (storyteller.tigris.org) 工具的作者。请访问他的博客的底纹树开发,在 codebetter.com/blogs/jeremy.miller,CodeBetter 网站的一部分。