Effective C# 原则24:选择申明式编程而不是命令式编程

JerryXia 发表于 , 阅读 (839)

与命令式编程相比,申明式编程可以用更简单,更清楚的方法来描述软件的行为。申明式编程就是说用申明来定义程序的行为,而不是写一些指令。在C#里,也和其它大多数语言一样,你的大多数程序都是命令式的:在程序中写一个方法来定义行为。在C#中,你在编程时使用特性就是申明式编程。你添加一个特性到类,属性,数据成员,或者是方法上,然后.Net运行时就会为你添加一些行为。这样申明的目的就是简单易用,而且易于阅读和维护。

让我们以一个你已经使用过的例子开始。当你写你的第一个ASP.Net
Web服务时,向导会生成这样的代码:

[WebMethod]
public string HelloWorld()
{
  return "Hello World";
}

VS.net的Web服务向导添加了[WebMethod]特性到HelloWorld()方法上,这就定义了HelloWorld是一个web方法。ASP.net运行时会为你生成代码来响应这个特性。运行时生成的Web服务描述语言(WSDL)文档,也就是包含了对SOAP进行描述的文档,调用HelloWorld方法。ASP.net也支持运行时发送SOAP请求HelloWorld方法。另外,ASP.net运行时动态的生成HTML面页,这样可以让你在IE里测试你的新Web服务。而这些全部是前面的WebMethod特性所响应的。这个特性申明了你的意图,而且运行时确保它是被支持的。使用特性省了你不少时间,而且错误也少了。

这并不是一个神话,ASP.net运行时使用反射来断定类里的哪些方法是web服务,当它们发现这些方法时,ASP.net运行时就添加一些必须的框架代码到这些方法上,从而使任何添加了这些代码的方法成为web方法。

[WebMethod]
特性只是.Net类库众多特性之一,这些特性可能帮助你更快的创建正确的程序。有一些特性帮助你创建序列化类型(参见原则25)。正如你在原则4里看到的,特性可以控制条件编译。在这种情况以下其它一些情况下,你可以使用申明式编程写出你所要的更快,更少错误的代码。

你应该使用.Net框架里自带的一些特性来申明你的意图,这比你自己写要好。因为这样花的时间少,更简单,而且编译器也不会出现错误。

如果预置的特性不适合你的需求,你也可以通过定义自己的特性和使用反射来使用申明式编程结构。做为一个例子,你可以创建一个特性,然而关联到代码上,让用户可以使用这个特性来创建默认可以排序的类型。一个例子演示了如何添加这个特性,该特性定义了你想如何在一个客户集合中排序:

[DefaultSort( "Name" )]
public class Customer
{
  public string Name
  {
    get { return _name; }
    set { _name = value; }
  }

  public decimal CurrentBalance
  {
    get { return _balance; }
  }

  public decimal AccountValue
  {
    get
    {
      return calculateValueOfAccount();
    }
  }
}

DefaultSort特性,Nane属性,这就暗示了任何Customer的集合应该以客户名字进行排序。DefaultSort特性不是.Net框架的一部份,为了实现它,你创建一个DefaultSortAttribute类:

[AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct )]
public class DefaultSortAttribute : System.Attribute
{
  private string _name;
  public string Name
  {
    get { return _name; }
    set { _name = value; }
  }

  public DefaultSortAttribute( string name )
  {
    _name = name;
  }
}

同样,你还必须写一些代码,来对一个集合运行排序,而该集合中的元素是添加了DefaultSort特性的对象。你将用到反射来发现正确的属性,然后比较两个不同对象的属性值。一个好消息是你只用写一次这样的代码。

下一步,你要写一个实现了IComparer接口的类。(在原则26中会详细的充分讨论比较。)
ICompare有一个CompareTo()方法来比较两个给定类型的对象,把特性放在实现了IComparable的类上,就可以定义排序顺序了。构造函数对于通用的比较,可以发现默认的排序属性标记,而这个标记是基于已经比较过的类型。Compare方法对任何类型的两个对象进行排序,使用默认的排序属性:

internal class GenericComparer : IComparer
{
  // Information about the default property:
  private readonly PropertyDescriptor _sortProp;

  // Ascending or descending.
  private readonly bool _reverse = false;

  // Construct for a type
  public GenericComparer( Type t ) :
    this( t, false )
  {
  }

  // Construct for a type
  // and a direction
  public GenericComparer( Type t, bool reverse )
  {
    _reverse = reverse;
    // find the attribute,
    // and the name of the sort property:

    // Get the default sort attributes on the type:
    object [] a = t.GetCustomAttributes(
      typeof( DefaultSortAttribute ),false );

    // Get the PropertyDescriptor for that property:
    if ( a.Length > 0 )
    {
      DefaultSortAttribute sortName = a[ 0 ] as   DefaultSortAttribute;
      string name = sortName.Name;

      // Initialize the sort property:
      PropertyDescriptorCollection props =
        TypeDescriptor.GetProperties( t );
      if ( props.Count > 0 )
      {
        foreach ( PropertyDescriptor p in props )
        {
          if ( p.Name == name )
          {
            // Found the default sort property:
            _sortProp = p;
            break;
          }
        }
      }
    }
  }

  // Compare method.
  int IComparer.Compare( object left,
    object right )
  {
    // null is less than any real object:
    if (( left == null ) && ( right == null ))
      return 0;
    if ( left == null )
      return -1;
    if ( right == null )
      return 1;

    if ( _sortProp == null )
    {
      return 0;
    }

    // Get the sort property from each object:
    IComparable lField =
      _sortProp.GetValue( left ) as IComparable;
    IComparable rField =
      _sortProp.GetValue( right ) as IComparable;
    int rVal = 0;
    if ( lField == null )
      if ( rField == null )
        return 0;
      else
        return -1;
    rVal = lField.CompareTo( rField );
    return ( _reverse ) ? -rVal : rVal;
  }
}

这个通用的比较对任何Customers
集合可以进行排序,而这个Customers是用DefaultSort特性申明了的:

CustomerList.Sort(new GenericComparer(typeof( Customer )));

实现GenericComparer的代码利用了一些高级的技术,使用反射(参见原则43)。但你必须写一遍这样的代码。从这个观点上看,你所要做的就是添加空上属性到其它任何类上,然而你就可以对这些对象的集合进行能用的排序了。如果你修改了DefaultSort特性的参数,你就要修改类的行为。而不用修改所有的算法。

这种申明式习惯是很有用的,当一个简单的申明可以说明你的意图时,它可以帮助你避免重复的代码。再参考GenericComparer类,你应该可以为你创建的任何类型,写一个不同的(而且是是直接了当的)排序算法。这种申明式编程的好处就是你只用写一次能用的类型,然后就可以用一个简单的申明为每个类型创建行为。关键是行为的改变是基于单个申明的,不是基于任何算法的。GenericComparer可以在任何用DefaultSort特性修饰了的类型上工作,如果你只须要在程序里使用一两次排序功能,就按常规简单的方法写吧。然而,如果你的程序对于同样的行为,可能须要在几十个类型上实现,那么能用的算法以及申明式的解决方案会省下很多时间,而且在长时间的运行中也是很有力的。你不应该为WebMethod特性写代全部的代码,你应该把这一技术展开在你自己的算法上。原则42里讨论了一个例子:如何使用特性来建立一个附加命令句柄。其它的例子可能还包括一些在定义附加包建立动态的web
UI面页时的其它内容。

申明式编程是一个很有力的工具,当你可以使用特性来表明你的意图时,你可以通过使用特性,来减少在大量类似的手写算法中出现逻辑错误的可能。申明式编程创建了更易于阅读,清晰的代码。这也就意味着不管是现在还是将来,都会少出现错误。如果你可以使用.Net框架里定义的特性,那就直接使用。如果不能,考虑选择创建你自己的特性,这样你可以在将来使用它来创建同样的行为。

添加新评论