Effective C# 原则24:选择申明式编程而不是命令式编程
Item 24: Prefer Declarative to Imperative Programming
与命令式编程相比,申明式编程可以用更简单,更清楚的方法来描述软件的行为。申明式编程就是说用申明来定义程序的行为,而不是写一些指令。在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框架里定义的特性,那就直接使用。如果不能,考虑选择创建你自己的特性,这样你可以在将来使用它来创建同样的行为。
===========================
Item 24: Prefer Declarative to Imperative Programming
Declarative programming can often be a simpler, more concise way to describe the behavior of a software program than imperative programming. Declarative programming means that you define the behavior of your program using declarations instead of by writing instructions. In C#, as in many other languages, most of your programming is imperative: You write methods that define the behavior of your programs. You practice declarative programming using attributes in C#. Youattach attributes to classes, properties, data members, or methods, and the .NET runtime adds behavior for you. This declarative approach is simpler to implement and easier to read and maintain.
Let's begin with an obvious example that you've already used. When you wrote your first ASP.NET web service, the wizard generated this sequence of code:
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}
The VS .NET Web Service wizard added the [WebMethod] attribute to the HelloWorld() method. That declared HelloWorld as a web method. The ASP.NET runtime creates code for you in response to the presence of this attribute. The runtime created the Web Service Description Language (WSDL) document, which contains a description for the SOAP document that invokes the HelloWorld method. ASP.NET also adds support in the runtime to route SOAP requests to your HelloWorld method. In addition, the ASP.NET runtime dynamically creates HTML pages that enable you to test your new web service in IE. That's all in response to the presence of the WebMethod attribute. The attribute declared your intent, and the runtime ensured that the proper support was there. Using the attribute takes much less time and is much less error prone.
It's really not magic. The ASP.NET runtime uses reflection to determine which methods in your class are web methods. When they are found, the ASP.NET runtime can add all the necessary framework code to turn any function into a web method.
The [WebMethod] attribute is just one of many attributes that the .NET library defines that can help you create correct programs more quickly. A number of attributes help you create serializable types (see Item 25). As you saw in Item 4, attributes control conditional compilation. In those and other cases, you can create the code you need faster and with less chance for errors using declarative programming. You should use these .NET Framework attributes to declare your intent rather than write your own code. It takes less time, it's easier, and the compiler doesn't make mistakes.
If the predefined attributes don't fit your needs, you can create your own declarative programming constructs by defining custom attributes and using reflection. As an example, you can create an attribute and associated code to let users create types that define the default sort order using an attribute. A sample usage shows how adding the attribute defines how you want to sort a collection of customers:
[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();
}
}
}
The DefaultSort attribute e, the Name property. The implication is that any collection of Customers should be ordered by the customer name. The DefaultSort attribute is not part of the .NET Framework. To implement it, you need to create the DefaultSortAttribute class:
[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;
}
}
You must still write the code to sort a collection of objects based on the presence of the DefaultSort attribute. You'll use reflection to find the correct property and then compare values of that property in two different objects. The good news is that you need to write this code only once.
Next, you create a class that implements IComparer. (Comparers are discussed in more detail in Item 26.) IComparerhas a version of CompareTo() that compares two objects of a given type, letting the target class, which implements IComparable, define the sort order. The constructor for the generic comparer finds the default sort property descriptor based on the type being compared. The Compare method sorts two objects of any type, using the default sort property:
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;
}
}
The Generic comparer sorts any collection of Customers based on the property declared in the DefaultSort attribute:
CustomerList.Sort( new GenericComparer(
typeof( Customer )));
The code to implement the GenericComparer makes use of advanced techniques, such as reflection (see Item 43). But you need to write it only once. From that point on, all you need to do is add the attribute to any class, and you can sort a collection of those objects using the generic comparer. If you change the parameter on the DefaultSort attribute, you change the class's behavior. You don't need to change any algorithms anywhere in your code.
This declarative idiom is useful to avoid writing repetitivecode when a simple declaration can specify your intent. Look at the Generic Comparer class again. You could write different (and slightly simpler) versions of the sort algorithm for every type you created. The advantage to using declarative programming is that you can write one generic class and let a simple declaration create the behavior for each type. The key is that the behavior changes based on a single declaration, not based on any algorithm changes. The GenericComparer works for any type decorated with the DefaultSort attribute. If you need sorting functionality only once or twice in your application, write the simpler routines. However, if you might need the same behavior for many tens of different types in your program, the generic algorithm and the declarative solution will save you time and energy in the long run. You'd never write all the code generated by the WebMethod attribute. You should expand on that technique for your own algorithms. Item 42 discusses one example: how to use attributes to build add-on command handlers. Other examples might include anything from defining add-on packages to building dynamic web page UIs.
Declarative programming is a powerful tool. When you can use attributes to declare your intent, you save the possibility of logic mistakes in multiple similar hand-coded algorithms. Declarative programming creates more readable, cleaner code. That means fewer mistakes now and in the future. If you can use an attribute defined in the .NET Framework, do so. If not, consider the option of creating your own attribute definition so that you can use it to create the same behavior in the future.