代码改变世界

Jumony(一)从扩展方法到流畅的程序体验

2010-07-24 02:38  Ivony...  阅读(7011)  评论(49编辑  收藏  举报

今天让公司的程序员试用了一下还在开发中的代号为"Jumony"的HTML数据绑定引擎,开发人员的一句评价被我视为最高的褒奖。

“感觉这个框架就是,你想到什么就写什么。”

 

想到什么就写什么,在这个越来越强调快速开发的时代,这一点变得越来越重要。我最近经常戏言:“natural code才是王道”,当然,不是说我们要用中文去编程,而是程序应该成为越来越自然的表达

 

让程序员获得流畅的编程体验,是将来每一个框架都必须去考虑和实现的事情。随着.NET Framework 3.5的普及,越来越多的.NET框架开始注重为程序员提供流畅的体验。为什么是随着.NET Framework 3.5的普及呢?因为在劣质的语言(如Java等)上,我们花费大得多的代价,也很难获得流畅的体验。

 

.NET Framework 3.5/C# 3.0增加了大量的新特性,lambda表达式和ExpressionTree自然是很强大的特性,不过在这里我特别想提的是扩展方法。

扩展方法的本质是实现函数的中缀表达式,自从有函数以来,我们就习惯了前缀函数表达式,像这样:

Console.Write( "Hello world!" );

这样的形式,对于命令式的程序来说,的确是比较合理的方式,但是如果我们考虑下面这个函数:

Add( 1, 2 );

就显得不那么友好,显然我们喜闻乐见的形式是1 + 2,这与运算符的书写形式同理,显然我们不喜欢+ 1 2或是1 2 +这样的前缀或后缀表达式。并且,这种函数在连起来使用的时候,就是灾难:

Add( ... Add( Add( Add( 1, 2 ), 3 ), 4 ) ... );

 

OO的出现带来了一个语法的革新,OO认为对象自己可以拥有自己的方法(函数)。我刚刚接触到OO的时候,最感兴趣的就是打开一个文件已经不需要这样:

FILE* fp = fopen( "C:\\Temp.txt", "w" );

fputs( fp, "123", 3 );

fclose( fp );

这些函数都成为了File自己的方法:

file->open( "C:\\Temp.txt", "w" );

file->puts( "123", 3 );

file->close();

我当时为了这个果断的抛弃了语法过时落后的C而投入了C++的研究之中。我当时甚至连OO是什么都不清楚,只知道结构体现在能够有自己的函数了,这真是一个不小的进步。

 

程序设计语言就是这样,不断地提高开发效率和带给程序员更好的书写体验。

对象的方法在一定程度上解决了函数中缀表达式的问题,代码也变得越来越好看,简单,易懂,结构性强。但很快我们就发现了这种方式的局限性。

 

对象的类型是一个静态的东西,一个对象拥有什么方法是设计这个对象的类型的时候决定的。大部分时候这没有问题,但随着我们需求的发展,我们发现有一些对象是不能决定自己有些什么方法的。

例如容器对象:decimal[]或是IList<decimal>或是IEnumerable<decimal>,我们会希望它拥有一个Sum方法,像这样:

IEnumerable<decimal> data = GetData();

var sum = data.Sum();

 

而显然的,我们不可能为T[]或是IList<T>或是IEnumerable<T>增加这样一个Sum方法。

这就使得我们不得不这样来写代码:

var sum = Math.Sum( data );

也许你会觉得这没什么,但事实上如果这种东西多了,你就会觉得很烦了:

var average = Math.Sum( data ) / Enumerable.Count( data );

如果是一个复杂的公式,光想想就够了。

 

当然,这是扩展方法的其中一个应用场景,也是几乎不可替代的一个场景。如果不写扩展方法,我们就不得不专门写一个DecimalCollection的类型出来。这些架子代码大量的充斥在我们的代码中的时候,我们会发现其实我们真正用来表述逻辑和真实意图的代码越来越少。

 

扩展方法的另一个应用场景就是在不同的上下文中,对象可能需要呈现出不同的方法。比如说我们在一个大量需要正则匹配和替换的场景,我们就会希望字符串可以直接调用正则表达式来替换最好:

str.Replace( @"\.html$", ".aspx" );

这是最符合我们预期的,但事实上我们必须写成:

Regex.Replace( @"\.html$", str, ".aspx" );

即使是这样的代码:

str.Replace( new Regex( @"\.html$" ), ".aspx" );

也是不允许的。

 

但当我们需要在一个表达式中进行多次正则替换时,代码就会变成一堆灾难。

 

这里面的原因很多,需要大量的正则替换的场景并不是通用常见场景,当然更重要的原因是,如果要为String实现string Replace( Regex, string )方法,必然导致String产生对Regex的依赖,而String是一个比Regex更为基础的类型,这种强绑定会带来很多的问题。例如,Regex必须与String放在同一个程序集中(否则会造成循环引用,因为Regex必然依赖String,事实上String在mscorlib中而Regex在System中)。除此之外,如果我们不想用微软的正则表达式引擎,那么我们还是不得不退回到原来的丑陋模式。

 

 

扩展方法的出现,解决了所有的这些问题,也使得我们的框架代码变得简洁,节省了大量的架子代码。Enumerable所定义的扩展方法只用了很少的代码就给我们带来了极大的便利。

 

为什么会忽然特别想聊扩展方法这个特性,因为我现在做的这个HTML数据绑定引擎中,程序员所调用到的方法大部分都是扩展方法。扩展方法加上接口,让我几乎不费力的就为程序员提供了所想即所写的编程体验。

不过在这里我不想太多的讨论这个东西,来谈谈扩展方法在其他方面的一些体验提高。

 

作为Web开发而言,用Request来接收参数是最普通不过的事情,但Request只能接收字符串,我们不得不写很多的转换代码:

int userId = int.Parse( Reuqest.QueryString["userId"] );

//...

这样的代码写惯了倒也没什么,但多了的确是个负担,所以我看到很多程序员为了偷懒,就不作强类型转换了,这样带来非常多的隐患。我提供了一个扩展方法来试图解决这个问题:

int userId = Request.QueryString["userId"].ParseTo<int>();

现在代码比之前要好一些了,因为程序员的思路不会被打断,而对原有的程序的升级,在后面加一个.ParseTo显然也不太费事,大家接受度就会比较好。

当然,有程序员A指出,这个什么QueryString实在是太长了,难写。

那么这样:

int userId = Request.HttpGet<int>( "userId" );

如果是POST传递的数据:

int userId = Request.HttpPost<int>( "userId" );

当然我们还可以做很多有用的扩展。

 

来看看数据库吧,当我们需要从数据库中取出一个值时,有时候会遇到DBNull的情况。DBNull与null不同,前者是一个合法的值,这个值经常会搞得我们很烦:

(int) DbUtility.Scaler( "SELECT MAX(ID) FROM Users" );

当这个表中不存在一条记录的时候,返回值就会是DBNull。对于这种情况,我们希望对DBNull做一个默认值,如果是null的话,这很好办:

(int) (DbUtility.Scalar( "SELECT MAX(ID) FROM Users" ) ?? 0);

利用C# 2.0中的??操作符,我们可以很容易办到这一点,但这对于DBNull却是无效的。

 

不过,扩展方法很容易做到这一点:

int id = DbUtility.Scalar( "SELECT MAX(ID) FROM Users" ).IfNull( 0 );

 

或是使得下面的丑陋的强制类型转换语法写起来顺手点:

((DateTime) dataItem).ToString( "yyyy-MM-dd" );

dataItem.Cast<DateTime>().ToString( "yyyy-MM-dd" );

 

 

扩展方法使得框架的编写者可以用很少的代码就能实现流畅的编码感受,我一直认为,一个框架是否好用,并不在于它的功能有多强大,而在于它是否给程序员提供了流畅的编程体验,和直觉性的代码书写。用我的话说就是:“当你第一眼看到这个方法,觉得它会是干什么的,会返回什么,那它就真的是干这个的,也真的会返回你想要的东西”,不要翻手册或是帮助,甚至不需要借助参数和方法的说明文字,你所想的即它所做的,这才是最重要的。

 

最后提供我的ParseTo扩展方法的完整实现:

  public static class WebExtensions
  {
    public static T ParseTo<T>( this string value )
    {
      if ( Parser<T>.ParseMethod != null )
        return Parser<T>.ParseMethod( value );


      throw new NotSupportedException();
    }

    static WebExtensions()
    {
      Parser<short>.ParseMethod    = short.Parse;
      Parser<int>.ParseMethod      = int.Parse;
      Parser<long>.ParseMethod     = long.Parse;
      Parser<byte>.ParseMethod     = byte.Parse;
      Parser<ushort>.ParseMethod   = ushort.Parse;
      Parser<uint>.ParseMethod     = uint.Parse;
      Parser<ulong>.ParseMethod    = ulong.Parse;
      Parser<sbyte>.ParseMethod    = sbyte.Parse;
      Parser<float>.ParseMethod    = float.Parse;
      Parser<double>.ParseMethod   = double.Parse;
      Parser<decimal>.ParseMethod  = decimal.Parse;
      Parser<bool>.ParseMethod     = bool.Parse;
      Parser<DateTime>.ParseMethod = DateTime.Parse;
      Parser<TimeSpan>.ParseMethod = TimeSpan.Parse;
    }

    private class Parser<T>
    {
      public delegate T ParseMethodDelegate( string value );


      private static bool noParseMethod = false;
      private static ParseMethodDelegate _parseMethod;

      public static ParseMethodDelegate ParseMethod
      {
        get
        {

          if ( _parseMethod != null )
            return _parseMethod;

          if ( noParseMethod )
            return null;

          var method = typeof( T ).GetMethod( "Parse", new Type[] { typeof( string ) } );
          if ( method != null && (method.Attributes & MethodAttributes.Static) != 0 )
          {
            DynamicMethod dynamicMethod = new DynamicMethod( typeof( T ).FullName + "_Parse", typeof( T ), new Type[] { typeof( string ) } );

            var il = dynamicMethod.GetILGenerator();
            il.Emit( OpCodes.Ldarg_0 );
            il.EmitCall( OpCodes.Call, method, null );
            il.Emit( OpCodes.Ret );

            return _parseMethod = (Parser<T>.ParseMethodDelegate) dynamicMethod.CreateDelegate( typeof( Parser<T>.ParseMethodDelegate ) );

          }

          noParseMethod = true;
          return null;
        }
        set
        {
          _parseMethod = value;
        }
      }
    }
  }