求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
设计模式自动化
 

发布于2013-7-11

 

简介

软件开发项目正在变得日趋庞大与复杂。越是复杂的项目,其软件开发与维护的成本越有可能远远超过花费在硬件上的成本。

软件的规模与其开发和维护的成本之间存在着一种超线性的关系。说到底,庞大且复杂的软件需要优秀的工程师进行开发与维护,而优秀的工程师总是难以吸引的,留住他们的代价也更高昂。

尽管维护每行代码的成本如此高昂,但我们依然编写了大量的样板代码,而这其中有很大一部分可以由更智能的编译器来替代完成。实际上,多数模板代码只是重复地实现设计模式,而其中一部分模式已被理解得十分透彻,只要我们教会编译器一些技巧,它们完全是可以自动实现的。

实现观察者模式

以观察者模式作为例子。这个模式在1995年就已被早早地提出了,并且成为了Model-View-Controller架构成功实现的基础。组成这个模式的各元素在首个版本的Java(1995,Observable接口)和.NET(2001,INotifyPropertyChanged接口)中都得到了实现。虽然这些接口都是框架中的一部分,但还是需要开发者的手动实现。

INotifyPropertyChanged接口仅包含一个名叫PropertyChanged的事件,当对象的任何一个属性值发生变化时,都需要触发该事件。

让我们来看一看一个简单的.NET示例:

public Person : INotifyPropertyChanged
{

  string firstName, lastName;
   public event NotifyPropertyChangedEventHandler PropertyChanged;

   protected void OnPropertyChanged(string propertyName)
  {
    if ( this.PropertyChanged != null ) {
         this.PropertyChanged(this, new 
PropertyChangedEventArgs(propertyName));
   }
  }

 public string FirstName
  {
   get { return this.firstName; }
  set
    {
       this.firstName = value;
       this.OnPropertyChanged(“FirstName”);
       this.OnPropertyChanged(“FullName”);
  }
public string LastName
  {
   get { return this.lastName; }
  set
    {
       this.lastName = value;
       this.OnPropertyChanged(“LastName”);
       this.OnPropertyChanged(“FullName”);
  }
  public string FullName { get { return string.Format( “{0} {1}“, 
this.firstName, this.lastName); }}}

属性最终依赖于一组字段,一旦我们改变一个字段,那我们就要为一个相关联的属性触发PropertyChanged事件。

难道编译器不能为我们自动完成这一工作吗?完整的答案是:如果我们考虑到所有可能发生的边界情况,那么要检测字段与属性之间的依赖确实是一项令人望而生畏的任务。因为属性所依赖的字段有可能指向其它对象,这些对象可以调用其它方法。更糟的是,它们还可能调用虚方法或delegate,而编译器却无法确定具体的类型。因此对这个问题来说,如果我们不希望编译时间达到几小时或几天,而是可以在几秒或几分钟内就完成的话,那确实不存在一个通用的解决方案。不过,在真实场景中,编译器是可以理解大多数简单的属性的。因此简短的回答是:是的,在典型的应用中,编译器可以为超过90%的属性生成通知代码。

在实践中,同样的类可以由以下方式实现:

[NotifyPropertyChanged]
public Person
{

public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get { return string.Format( “{0} {1}“, 
this.FirstName, this.LastName); }}

}

这段代码告诉了编译器要做什么(实现INotifyPropertyChanged),而不是该怎样做。

样板代码是一种反模式

观察者(INotifyPropertyChanged)模式仅是在大型应用程序中产生大量样板代码的一个例子,而在典型的代码库中经常充斥着实现各种模式的大量样板代码。即使它们并不总是被认可为“官方的”设计模式,但它们依然是模式,因为它们在代码库中经常重复不断地出现。最常见的代码重复的原因包括:

1、追踪、日志

2、前置条件与不变式的检测

3、授权与审计

4、锁定与线程分配

5、缓存

6、跟踪变化(以实现撤消/重做)

7、事务处

8、异常处理

这些特性难以用寻常的面向对象技术进行封装,这也是造成了它们经常用样板式代码实现的原因。这真的是一件那么糟糕的事吗?

确实是。

使用样板代码解决横切关注点(cross-cutting concerns),会最终导致其违反优秀软件工程应遵守的基本原则:

1、当一个单一属性的setter方法中的实现囊括了多个关注点的内容,如验证、安全、INotifyPropertyChanged及撤消/重做时,单一职责原则即被违反。

2、能够在不修改现有代码的情况下加入新的特性,才是最好地实现了开闭原则,该原则指出,软件实体应该对扩展开放,而对修改关闭。

3、不要重复你自己(DRY)原则不能容忍因手工实现设计模式所带来的代码重复。

4、当由于某个模式的实现难以更改,而不得不手动实现时,就违反了松耦合原则。请注意,耦合不仅仅产生于两个组件之间,也会产生在组件和概念设计之间。将一个类库替换为另一个实现了相同概念设计的库通常是比较容易的,但要切换为一个不同的设计则需要多得多的源代码改动。

除此之外,样板会使你的代码:

1、在试图理解代码如何实现功能需求时,你会发现它难以阅读并理解其原因。考虑到阅读代码在软件维护中占用了75%的时间,这一层无谓的复杂性对于软件维护是个巨大成本消耗。

2、代码更庞大,这不仅意味着生产力的降低,也意味着开发与维护软件的成本提高,更不用说产生bug的风险也增加了。

3、难以重构及修改。修改一段样板代码(通常是为了修复某个bug)意味着所有应用该样板的地方都需要一起改变。当某个样板库可能横跨多个解决方案或者是源代码库时,你又如何准确地指出该样板到底在你的整个代码中的哪些地方被用到呢?莫非你打算进行查找与替换吗?

如果你将那些样板代码置之不理,那它们就会像杂草一样爬满你的代码,每次应用到某个新方面时,都会占用更多的空间。直至某日,你的代码库将会被样板代码所占满。在我之间待过的某个团队中,一个简单的数据访问层的类就有超过1000行的代码,而其中90%的代码是处理各种SQL异常及重试的样板代码。

我希望你现在已经理解了为什么使用样板代码实现模式是一种糟糕的方式。它实际上是一种反模式,因为它会导致不必要的复杂性、bug、高昂的维护代价、生产力的缺失并最终导致更高的软件成本。

设计模式自动化与编译器扩展

很多时候,我们纠结于创建可重用的样板代码的原因,是由于C#和Java这样的主流的静态类型语言缺乏对于元数据编程的原生支持。

编译器能够获取许多我们在编码时无法得知的信息,如果我们可以从这些信息中受益,并且通过编写编译器扩展以帮助我们实现设计模式,那不是很好吗?

一个更智能的编译器能允许我们实现以下几点:

1、编译时程序转换: 能够使我们在维持现有代码的语义、复杂性以及代码行数的前提下加入新特性,从而使我们能够自动实现某个设计模式中可自动化的一部分。

2、静态代码检验:为保证编译时安全,它将确保我们正确地使用了设计模式,或是检查某个模式中不能实现自动化的那些部分是否已按照一系列预定义的规则正确地实现了。

示例:C#中的“using”和“lock”关键字

如果你需要证据表明编译器能够直接支持设计模式,只需看看using和lock关键字就可以了。乍一看,这些关键字在C#语言中似乎是多余的,但C#的设计者们意识到了它们的重要性,并专门为它们创建了特有的关键字。

让我们看一看using关键字,它实际上是整个Disposable模式的一部分,由以下参与者所组成:

1、资源对象:占有任何外部资源的对象,例如一个数据库连接。

2、资源占用者:在某个特定的生命周期中占有资源对象的指令块或者是对象。

Disposable模式的规则由下列原则构成:

1、资源对象必须实现IDisposable接口。

2、IDisposable.Dispose方法的实现必须是冥等的(无副作用的),例如,它可以被安全地调用任意多次。

3、资源对象必须包括一个终结器(在C++中叫做析构器)。

4、IDisposable.Dispose方法的实现必须调用GC.SuppressFinalize方法。

5、通常来说,如果某个对象的字段指向一个资源对象,那么该对象本身也成为资源对象。子资源对象应该由它的父对象来清除。

6、分配及占用一个资源对象的指令块必须由using关键字进行修饰(除非对资源的引用是保存在对象本身的状态中,请参见上一点)

如你所见,Disposable模式实际上比它第一眼看上去要复杂得多。这个模式是怎样自动化并强制地实现的呢?

1、NET核心类库提供了IDisposable接口。

2、C#编译器提供了using关键字,它会在编译时自动生成某些代码(即一个try/finally语句块)。

3、可以使用FxCop定义一个强制的规则,要求所有disposable的类必须实现终结器,并且Dispose方法必须调用GC.SuppressFinalize。

因此,Disposable模式完美地表现了.NET平台可以直接支持设计模式的实现。

那么那些没有原生支持的模式呢?它们可以通过组合使用类库及编译器扩展来实现。我们的下一个示例同样来自Microsoft。

示例:代码契约

一直以来,对前置条件进行检验(也可选择性地检验后置条件及不变式)被认为是一种能够避免某个组件中的缺陷造成另一个组件出错的最佳实践。具体思路是这样的:

1、每个组件(一般来说是指每个类)应该被设计为一个“单元”;

2、每个单元为它自己的健壮性负责;

3、每个单元都要检查任何一个来自其它单元的输入;

检验前置条件可以被认为是一种设计模式,因为它是对一个不断发生的问题的可重复的解决方案。

Microsoft的代码契约就是设计模式自动化的一个完美的例子。它基于原生C#或Visual Basic,为你提供一组API以表达检验规则,规则的具体形式包括前置条件、后置条件和对象不变式。不过,该API不仅仅是一个类库,它还会为你的程序进行编译时转换及检验。

我不打算深入讲解代码契约过于细节的部分,简单地说,它允许你在代码中指定检验规则,并能够在编译时及运行时进行检查。举例来说:

public Book GetBookById(Guid id)
{
    Contract.Requires(id != Guid.Empty);

    return Dal.Get(id);
}

public Author GetAuthorById(Guid id)
{
    Contract.Requires(id != Guid.Empty);

    return Dal.Get(id);
}

它的二进制重写工具能够(基于你的设置)重写你编译出的程序集,并注入额外的代码以检查你所设定的各种条件。如果检查一下由二进制重写工具所转换后的代码,你将会看到类似如下代码:

public Book GetBookById(Guid id)
  {
      if (__ContractsRuntime.insideContractEvaluation <= 4)
      {
          try
          { 
              ++__ContractsRuntime.insideContractEvaluation;
              __ContractsRuntime.Requires(id != Guid.Empty, (string)null, "id !=
Guid.Empty");
          }
          finally
          {
              --__ContractsRuntime.insideContractEvaluation;
          }

      }
      public Author GetAuthorById(Guid id)<
  {
      if (__ContractsRuntime.insideContractEvaluation <= 4)
      {
          try
          {
              ++__ContractsRuntime.insideContractEvaluation;
              __ContractsRuntime.Requires(id != Guid.Empty, (string)null, "id !=
Guid.Empty");
          }
          finally
          {
              --__ContractsRuntime.insideContractEvaluation;
          }
      }
      return Dal.Get(id);   }

关于Microsoft代码契约的更多信息,请在这里阅读Jon Skeet在InfoQ上的优秀文章。

像代码契约这样的编译期扩展固然很好,但官方推出的扩展往往要花费数年的时间进行开发,直至成熟与稳定。由于存在着这么多不同的领域,每个领域又有着它自身的问题,官方的扩展是不可能覆盖所有这些问题的。

我们所需要的是一个通用框架,它能以一种纪律性的方式自动化并强制实施设计模式,使得我们自己能够更有效地解决特定于领域的问题。

自动化并强制实施设计模式的通用框架

人们可能会想到动态语言、开放式编译器(如Roslyn)或重编译器(如Cecil)等解决方案,因为它们都暴露了抽象语法树的深度细节。但是这些技术是高度抽象层面的操作,导致使用它们实现任何转换都非常复杂,只能用于最简单的一部分。

我们所需要的是一个编译器扩展的高层次的框架,它基于以下原则:

1、提供一系列转换基元,例如:

1)注入方法调用;

2)在方法执行之前及之后运行代码;

3)注入对字段、属性或事件的访问;

4)为某个现有类加入接口实现、方法、属性或事件。

2、提供某种方式,以表达基元应该应用到何处:告诉编译扩展你需要注入一些代码固然是好事,但更好的是我们能得知哪些方法应该被注入!

3、基元必须能够被安全地组合在代码中的相同位置应用多种转换是很自然的需求,因此这个框架应该给我们一种组合这些转换的能力。

当你能够同时应用多种转换时,某些转换也许需要按照特定的顺序进行。因此转换的顺序需要遵循一个定义良好的约定,并且允许我们在适当时重写默认的顺序。

4、扩展代码的语义应该不受影响

转换机制应该保持低调,并尽量减少对原始代码的改动,同时提供对转换进行静态检验的功能。这个框架应该让源代码的意图不要被轻易地“破坏”。

5、高级的反射与检验功能

按照定义,设计模式应该包含如何实现它的规则。例如,锁定设计模式应该规定实例字段只能被同一对象的实例方法所访问。这个框架必须提供一种机制以查询方法对某一给定字段的访问,并提供一种方式以产生整洁的编译时错误。

面向方面编程(AOP)

面向方面编程是一种编程范式,它旨在于通过允许关注分离以提高模块化。

“方面”(Aspect)是一种特殊的类,它包括了代码转换(称为通知(Advice))、代码匹配规则(粗略地称为切入点(Pointcut))以及代码检验规则。设计模式通常由一到多个方面实现。将方面应用到代码有多种方式,这主要取决于AOP框架的实现。定制特性(Java中的注解(Annotation))是一种为所选的代码元素加入方面的便利方式,而更复杂的切入点可以由XML声明式地表达(例如Microsoft Policy Injection Application Block)、或一门领域特定语言(例如AspectJ或Spring)进行表述、或使用反射(例如由LINQ配合PostSharp调用System.Reflection)编程实现。

编织(Weaving)过程将通知与初始源代码在特定的位置(一样粗略地称为连接点)组合在一起,它能够访问初始源代码的元数据,因此对于C#或Java这样的编译语言来说,它就为静态编织者提供了一个执行静态分析的机会,以确保通知与它所应用之处的切入点两者之间关联的有效性。

虽然面向方面编程与设计模式是各自独立的概念,但AOP对于那些致力于实现设计模式自动化或强制实施设计规则的人来说是个很好的解决方案。与低层次的元数据编程不同,AOP是按照以上介绍的原则设计的,因此不仅仅是编程器专家,任何人都可以通过它实现设计模式。

AOP是一种编程范式而不是一门技术,也因此它可以通过不同方式实现。在Java阵营中领先的AOP框架AspectJ,现在已经由Eclipse Java编译器直接实现了。而在.NET阵营中,由于编译器未开源的缘故,实现AOP最好的方式是重编译器,将C#或Visual Basic编译器的生成结果进行转换。在.NET中领先的工具是PostSharp(见下)。作为替代方式,某些AOP的子集可以通过动态代理及服务容器(service container)实现,并且多数依赖注入框架都至少能够提供方法注入的实现。

示例:使用PostSharp定制设计模式

PostSharp是在Microsoft .NET中自动化并强制实施设计模式的一项开发工具,并以.NET平台下最完整的AOP框架而闻名。

为了避免把这篇文章变成PostSharp的入门指导,还是让我们来看一个非常简单的模式吧:在一个前台(UI)线程和后台线程中反复地分配某个方法调用。该模式可以由两个简单的方面实现:一个方面将方法调用发送至后台线程,而另一个将方法调用发送至前台线程。这两个方面都可以由免费的PostSharp Express编译。首先来看一下第一个方面:BackgroundThreadAttribute。

该模式的生成部分非常简单:我们只需创建一个Task以执行方法体,并调度这个Task的执行。

[Serializable] 
public sealed class BackgroundThreadAttribute : MethodInterceptionAspect     
{   
    public override void OnInvoke(MethodInterceptionArgs args)   
    {   
        Task.Run( args.Proceed );   
    }   
}

MethodInterceptionArgs类包含了方法调用的上下文信息,例如参数及返回值。你可以利用这些信息调用原始方法,缓存它的返回值,记录它的输入参数,或者你的用例所要求的任何部分。

对于该模式的检验部分,我们希望避免将这个定制特性应用到那些具有返回值或是具有某个引用传递的参数的方法上。如果这种情况发生,我们将希望生成一个编译时错误。因此,我们必须在我们的BackgroundThreadAttribute类中实现CompileTimeValidate方法:

// Check that the method returns 'void', has no out/ref argument.
public override bool CompileTimeValidate( MethodBase method )
{

  MethodInfo methodInfo = (MethodInfo) method;

  if ( methodInfo.ReturnType != typeof(void) || 
       methodInfo.GetParameters().Any( p => p.ParameterType.IsByRef ) )
  {
     ThreadingMessageSource.Instance.Write( method, SeverityType.Error, 
"THR006",
             method.DeclaringType.Name, method.Name );

     return false;
  }

  return true;
}

ForegroundThreadAttribute看上去也差不多,它使用WPF中的Dispatcher对象,或是调用WinForms中的BeginInvoke方法。

以上两个方面可以像其它的attribute一样应用,例如:

[BackgroundThread]
private static void ReadFile(string fileName)
{
    DisplayText( File.ReadAll(fileName) );
}
[ForegroundThread]
private void DisplayText( string content )
{
   this.textBox.Text = content; 
}

最终源代码会比我们直接调用Task或Dispatcher的方式简洁许多。

有人可能会争辩道,C# 5.0已经用async和await关键字更好地解决了这个问题。没错,这也是很好的例子,表现了C#团队如何找到一个重复发生的问题,并决定通过在编译器和核心代码库中直接实现某个设计模式以解决该问题。只是.NET的开发者社区必须等到2012才能得到这个方案,而PostSharp早在2006年就提供这个功能了。

.NET社区还需要为其它通用设计模式的方案等待多久呢?例如INotifyPropertyChanged?那些特定于你的公司的应用框架的设计模式又怎样呢?

更智能的编译器能允许你实现你自己的设计模式,提高你的团队的生产力也不再依赖于编译器提供商了。

AOP的不足之处

我希望我已经说服了你,AOP是自动化设计模式与强制良好设计的一种解决方案,不过,最好能了解到它也存在着一些不足:

1、缺乏人员储备

作为一种范式,AOP并不是一门本科课程的内容,即使在硕士课程中也极少触及。这方面教育的缺乏也一定程度导致了开发者社区内对AOP缺乏一般性的认识。

尽管AOP已经出现了20年,它依然被误解为一门“新的”范式,这一点经常被证明为许多开发团队不敢采用它的最大障碍,只有最敢冒险的开发团队才敢于应用它。

设计模式存在的年限也差不多,但设计模式可以被自动实现及检验的想法是近期才出现的。我们在本文中举例说明了一些有意义的先进概念,包括C#编译器、.NET类库以及Visual Studio Code Analysis(FxCop)等等,但这些先进概念还未被归纳为设计模式自动化的一种通用实现。

2、惊讶的事实

由于人员和学生缺乏足够的准备,当他们应用AOP时也许会遇到各种Surprise,因为应用程序中包含了一些附加的行为,而这些行为从源代码中不能直接观察到。注意:所谓令人惊讶的部分,是AOP所期望的效果,这是由于编译器做了些比通常更多的事,而不是指它产生了任何副作用。

也有某些惊讶是来自于未预计到的效果,某个方面(或某个切入点中)包含的bug可能会导致转换被应用到预计之外的类与方法上。调试这种错误可能会十分微妙,尤其在开发者未意识到某个方面被应用到这个项目中的情况下。

这些惊讶的事实可以由这些方法解决:

1、IDE集成,这有助于以可视化的方式(a)在编辑器中显示哪些附加特性被应用到代码中(b)显示某个指定的方面被应用到哪些代码元素中。在编写本文的时候,还只有两个AOP框架提供对IDE良好的集成:AspectJ(配合AJDT plug-in使用于Eclipse中)与PostSharp(使用于Visual Studio中).

2、开发者的单元测试。方面本身以及它是否被正确地应用,必须和其它源代码一样进行单元测试。

3、在为代码应用方面时,不要依赖于命名约定,而是依赖于代码的组织特性,例如类继承或custom attribute。注意,这一讨论并不仅限于AOP,基于约定的编程在近期获得了广泛关注,虽然它的应用也伴随着许多Surprise的产生。

4、政策

使用设计模式自动化一般来说是一种敏感的政策问题,因为它也在一个团队中强调了关注分离的方式。通常情况下,高级开发者会选择设计模式并实现为方面,而初级开发者仅仅是应用它。高级开发者还会编写检验规则,以确保手写的代码符合架构规范。初级开发者不需要了解整个代码结构的这一事实,其实也是所预期的效果。

处理这一争论通常是比较微妙的,因为它是从一个高级管理者的角度出发,而往往会伤害到初级开发者的自尊心。

PostSharp模式库中现成的设计模式实现

如同我们从Disposable模式中所看到的,即使是看上去很简单的设计模式实际上也可能需要复杂的代码转换或验证。某些转换和验证虽然复杂,但还是有可能自动实现的。而其它部分可能对于自动处理来说过于复杂,而不得不手动完成。

幸运的是,通过使用AOP框架,还是有些简单的设计模式(异常处理、事务处理及安全等等)是每个人都可以轻易地实现为自动化的。

经过多年的市场经验,PostSharp团队意识到多数客户都在重复地实现相同的方面,于是他们开始为大多数通用的设计模式提供了高精度并且优化的现成实现。

PostSharp目前已为以下设计模式提供了现成的实现:

1、多线程:读写同步(reader-writer-synchronized)线程模型,角色(Actor)线程模型,线程独占模型,线程调度;

2、诊断:为各种后台类型,如NLog及Log4Net等提供高性能并且详细的日志记录功能;

3、INotifyPropertyChanged:包括对组合属性的支持以及对其它对象的依赖的支持;

4、契约:参数、字段及属性的检验。

现在,使用这些现成的设计模式的实现,开发团队在不必学习AOP的情况下就可以开始享受AOP所带来的好处了。

总结

像Java和C#这样所谓的高级语言,它依然强制要求开发者在一个不恰当的抽象层面编写代码。由于主流编译器的限制,开发者被迫编写许多样板式代码,这给应用程序的开发和维护都加重了负担。样板源自于对模式的各种混乱的手工实现,这也许是代码复制-粘贴在此行业中延续至今的使用最多的情况。

未能实现设计模式的自动化或许使得软件行业平白消耗了数十亿美元,也使得那些软件工程师花费了大量的时间在处理结构体系上的问题,而不是将时间花在增加商业价值上。

不过,如果有更智能的编译器允许我们自动实现大多数的通用模式,那大量的样板代码就可以被消灭了。希望未来的语言设计者能够领会到:设计模式是现代化软件开发的一等公民,并且应该在编译器中得到适当的支持。

但实际上并不需要等待新编译器的出现,它们不仅存在,并且已经很成熟了。面向方面编程方法就是为解决样板代码的问题而特别设计的。AspectJ和PostSharp都是这些理念成熟的实现,并且它们已使用在世界上几个最大的公司里了。并且PostSharp和Spring Roo都提供了大多数通用模式的现成的实现。一如既往,先行者能比其它追随者提早好几年获得生产力的提升。

在四人组的设计模式一书面市18年之后,设计模式也该成年了吧?

相关文章

为什么要做持续部署?
剖析“持续交付”:五个核心实践
集成与构建指南
持续集成工具的选择-装载
 
相关文档

持续集成介绍
使用Hudson持续集成
持续集成之-依赖管理
IPD集成产品开发管理
相关课程

配置管理、日构建与持续集成
软件架构设计方法、案例与实践
单元测试、重构及持续集成
基于Android的单元、性能测试
 
分享到
 
 


重构-使代码更简洁优美
Visitor Parttern
由表及里看模式
设计模式随笔系列
深入浅出设计模式-介绍
.NET中的设计模式
更多...   

相关培训课程

J2EE设计模式和性能调优
应用模式设计Java企业级应用
设计模式原理与应用
J2EE设计模式指南
单元测试+重构+设计模式
设计模式及其CSharp实现


某电力公司 设计模式原理
蓝拓扑 设计模式原理及应用
卫星导航 UML & OOAD
汤森路透研发中心 UML& OOAD
中达电通 设计模式原理
西门子 嵌入式设计模式
更多...