2007年6月3日星期日

Iris:Web Page Decoration Framework

(实验性项目,已不再维护)

Iris是为了处理Web页面装饰和布局管理的框架,它的设计思想基于GoF 的decoraotr设计模式。Iris分离页面正文和布局装饰的部分,使得Web页面更容易被测试和维护。

如果要了解Iris的更多信息请看Iris IntroduceIris Developer Guide

Features

  • 支持多级装饰器
  • 支持可扩展的decorator filter结构
  • ASP.Net技术
  • C#实现

下载

Enum还是Enum Class(枚举类)[.Net]

常量/枚举类型的表示

系统中常常有一些属性的属性值是固定的一组值,它们的值域是封闭的(有限数量),比如国家代码(每个国家具有唯一的代码,而在一定时期国家的数量是确定的)、性别类型(男、女)。在现代程序语言中,一种典型的表示方式是枚举类型(Enum)。Enum表示封闭值域的类型,常常由程序语言作为一种数据类型直接支持,例如C,C#等。C#支持的enum在C的基础上提供了类型安全的能力,下面是用C#定义的性别枚举类型:


public enum Sex {
Male,
Female,
}

Java不支持enum数据类型,Java认为C提供的enum并不是类型安全的,通常使用称之为Typesafe Enum Class的设计模式来获得类似的效果(参见[Joshua01] P80,Item21 :Replace enum constructs with classes)。Enum Class不允许外部构造实例成员(构造函数为private),提供静态类型成员实例来表示封闭值域。使用Enum Class方式来表示Sex类型可定义如下(C#):


public class Sex{
// 私有构造保证值域的封闭性
private Sex() { }

pubic static readonly Sex Male = new Sex():
pubic static readonly Sex Female = new Sex():

}

同enum一样,可以使用Sex.Male或Sex.Female的方式来访问常量属性,与静态常量字段不一样(如静态字符串、整数),enum和Enum Class可以提供强类型的compile time检查以及提供更好的数据封装性和代码可读性。例如使用常量类型设置和比较属性值:

// 设置属性值
Sex sex = Sex.Male;
// 比较
if (sex == Sex.Male) {
// ... ...
}

如果Sex是使用Enum定义的,则上面比较的实际上是Enum字段的值;如果Sex是使用Enum Class定义的,则比较的是静态实例成员的引用地址,当然也可以使用Equals方法来比较。

虽然Enum Class是来自于Java的设计模式,但在C#中并非没有意义,因为Enum Class提供了比Enum类型更强大的能力。

Enum与Enum Class的比较

Enum与Enum Class均提供了封装常量的能力,都能够实现编译时的强类型检查,使用封闭值域防止非法值。不过,因为实现机制的不同,这两种方式也具有不同的特点。

Enum在C#中是一种值类型(Value Type),其基类型必须是整数类型(如Int16),因此Enum也具有值类型所具有的优点——比引用类型(Reference Type)更高的效率,定义简单。C#的Enum还支持位(Bit)操作,对于用于标志的常量,允许常量的OR、AND、XOR、NOT等位操作,C#提供了内建的语言支持,通过FlagsAttribute即可使Enum具备位操作能力。例如.Net Framework的System.AttributeTargets属性,允许Attribute子类在设置AttributeUsage时可以组合AttributeTargets属性(参见.Net Framework SDK):

// 使用Flags枚举
// 下面的定义表示CustomerAttribute可以作用在Class或者Method上
[AttributeUsage(AttributeTargets.Class AttributeTargets.Method )]
public sealed class CustomAttribute : Attribute {
... ...
}

Enum的缺点不能实现自定义的行为,无法提供常量更多的属性,不过在大多数情况下,这已经足够了。Enum Class没有这种限制,虽然Enum Class本身并不设计为可以继承,但可以修改基类(System.Object)的行为以提供更加丰富的能力(如修改ToString方法,根据使用者的本地语言输出本地化的国家名称),也可以提供更多的属性 。例如我们提供一个候选的国家列表,除了能显示国家名称外,可以提供国家代码、语言代码信息。关于Enum Class更多的应用场景后面的节有详细的描述。

Enum Class的问题

上面的Enum Class实现方式也有它的缺点,在上面的设计中Enum Class通过进程内静态成员引用地址相同的机理来比较 枚举量是否相等,但是当将一个序列化后的Enum Class实例反序列化后,CLR会创建一个新的实例,从而造成反序列化值不等于序列化前值的现象:

IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

MemoryStream stream = new MemoryStream();
// 序列化Sex.Male的值
formatter.Serialize(stream, Sex.Male);
stream.Seek(0,SeekOrigin.Begin);
// 反序列化
Sex sex = (Sex)formatter.Deserialize(stream);
Console.WriteLine(sex == Sex.Male);

上面的代码将输出false。因此通过引用的方式是有局限性的,在Java中这是一个比较棘手的问题,需要修改反序列化的行为(参看[Joshua01]P171)来保证进程中只存在一个枚举量的唯一实例。C#与Java的反序列化实现机制不一样,无法通过修改反序列化的行为来返回同一个常量实例 ,但C#提供了操作符重载的能力。我们可以通过重载操作符“==”来解决这个问题,同时为了保持CLS兼容以及与Equals的行为一致,还需要改写Equals方法:


[Serializable]
public class Sex{
// 性别类型名
private string sexName;
// 私有构造保证值域的封闭性
private Sex(string sexName) {
this.sexName = sexName;
}
public static readonly Sex Male = new Sex( "Male");
public static readonly Sex Female = new Sex( "Female");

// 提供重载的 "=="操作符,使用sexName来判断是否是相同的Sex类型
public static bool operator ==(Sex op1, Sex op2) {
if (Object.Equals(op1, null)) return Object.Equals(op2, null);
return op1.Equals(op2);
}

public static bool operator !=(Sex op1,Sex op2) {
return !(op1 == op2);
}

public override bool Equals(object obj) {
Sex sex = obj as Sex;
if (obj == null) return false;
return sexName == sex.sexName;
}

public override int GetHashCode() {
return sexName.GetHashCode ();
}
}

通过操作符重载,不再使用引用地址来比较常量,而是通过值比较(如上面的sexName),因此要求每个常量实例必须具有唯一的标识值。 在不支持操作符重载的语言中,不能使用"=="来比较两个常量值是否相等,而应该使用Equals方法来代替。

Enum Class的设计

Enum Class一般符合下列规则:

  • 私有构造函数,保证外部无法创建类实例(同时也使得类无法继承)。

  • 静态只读实例字段表示常量。

  • 重载操作符"==",保证序列化后的值也能比较相等。当需要在进程间传递(如分布式应用)或需要序列化时,必须实现"=="操作符的重载。

  • 改写Equals方法,保持"=="行为和Equals一致。(改写Equals一般也同时改写GetHashCode方法 )

除此之外,还通常改写ToString方法以提供显示友好的名字,因为Java和.Net都在绑定或显示对象时使用ToString方法(Java中为toString方法)输出作为缺省的对象显示字符串,比如将Sex数组绑定到ListBox或者使用Console.Write输出时。下面的代码改写ToString方法以提供友好显示的输出:

public class Sex{
... ...
public override string ToString() {
return sexName;
}
}

当然我们也可以利用ToString提供本地化支持,返回本地语言的字符串。

Enum Class另外一种常见的职责是提供不同值系统之间的类型转换,如当从数据库中读取值时,利用Parse方法将数据库中值转换为对象系统的常量实例,而在存储时提供方法转换为数据库的值类型:


public class Sex{
... ...
// 根据一个符合指定格式的字符串返回类型实例。
public static Sex Parse(string sexName){
switch (sexName) {
case "Male" : return Male;
... ...
}
}

// 返回数据存储的值。
public string ToDBValue(){
return sexName;
}
}

使用Enum还是Enum Class?

根据Enum和Enum Class的特点,我们可以根据对常量类型的要求决定使用Enum还是Enum Class。

以下场景适合使用Enum:

  • 常量类型用于内部表示,不用于显示名字。

  • 常量值不需要提供附加的属性。例如只需要知道国家代码,而不需要获得国家的其它属性

  • 枚举值允许组合(即支持位操作)。


Enum Class可以适用于更多的场景:

  • 常用于可提供友好信息的类型。如本地化支持的类型名显示,或者显示与枚举名不一致的名字,例如Country.CHN可显示为"China"。

  • 提供更多的常量属性。

  • 提供更加丰富的行为。如Parse方法。

  • 对常量进行分组。如Country.Asia包含亚洲国家。

使用Struct来表示枚举

如果值域不封闭,但希望提供一些常量,也可以使用struct,如System.Drawing.Color结构中的系统默认颜色设置。采用struct来设计enum值同Enum Class方式没有本质的差异,只是struct默认提供无参数构造函数,因此无法实现封闭值域。


-------------
[Joshua01] Effective Java Programming Language Guide , Joshua Bloch, Pearson Education,2001.Java 高效编程指南(中文版),机械工业出版社,2002

2007年6月1日星期五

零.壹.无穷规则(Zero-One-Infinity Rule)

这是来自于黑客的行话(jargon),对于软件设计来说还是很有借鉴意义的一条经验原则,作为一种推演法的思考方式,相信对于其他领域应该也是有所裨益的。

黑客行话档案(Jargon Files)这样介绍的:


"不允许foo实例,允许一个foo实例或者允许任意的foo实例。"(译者注:软件开发者通常使用foo作为类设计的示例),一个软件设计的大拇指规则(Rule of Thumb),告诫不要随意的限制一个给定实体的实例数(例如:窗口系统的窗体数,操作系统文件名的字符数等)。更明确一些,一个实体要么完全不允许,要么允许恰好一个实例(一个“特例”),或者允许任意用户需要的——如地址空间或者内存使用。

隐藏在规则背后的逻辑是指在大多数情况下可以很清楚的知道需要一个而不是无。然而,如果更进一步,允许N(N>1),那为什么不是N+1呢?如果允许N+1,那为什么不是N+2?一旦超过1,并没有理由不允许任意的一个N;因而,就应当是无穷。

很多黑客还记得艾萨克.阿西莫夫(注1)的科幻小说"The Gods Themselves"中的一个人物说2是不会发生的——如果你相信有超过一个的宇宙,那么你也可以相信有无限个宇宙。


原文参见:Online Jargon Files

真的不存在"2"或特定的"N"吗?自行车有两个轮子,但也有三轮的自行车,四轮也未尝不可,似乎合理。但性别呢?方向(东南西北/左右/上下)?似乎在我们的认知条件下或从某种角度,也存在一些事物有固定的实体数量。

在数学和哲学领域我们可以很容易的表达无穷,它只是一个符号或一种象征,但在真实世界里通常是无法表达的。地球上的各种物质资源都是有限的,宇宙恐怕也是,即便一颗遥远恒星的重量超过我们的想象。有名的计算机"千年虫"问题,因为当时的设计者关于日期位数不周全的限制而带来了一场二十世纪末的恐慌,那么现在是否就彻底解决了这种限制了呢?现在的计算机时钟几百年、几千年后也不会再出现这个问题,但是千万年、亿年后呢?目前采用的年数表达方式也会出现"千万年虫问题"或者"亿年虫问题",因为计算机的资源是有限的,并不能真正表达一个无限的日期。就这个问题来说,这种考虑是没有必要的,没有人会担心这个问题——如果一百年或者几百年不会出现问题,那么就解决了,遥远的未来谁想去知道呢?大多数时候,“无限”并非真正需要。

在设计中考虑Zero-One-Infinity规则,有助于系统获得更好的扩展性。例如一个软件要适应两个领域或被五个用户使用,一个模块被两个或更多的地方调用,那么就应当考虑适应更多的领域,满足更多用户的通用特征,为模块提供扩展的机制以便更多的地方可以调用。

因此,我们在设计时考虑Zero-One-Infinity规则以消除对将来扩展不利的限制,而实现时则需要考虑在有限资源和价值下的"Infinity",采用有限的方式来表达无限,例如根据请求分配内存(对用户感觉是无限大)、无上限集合,一个足够大的数组,以及可扩展的结构。

很多经验开发方法,例如极限编程(eXtreme Programming),还强调平衡考虑扩展性的时机和代价,从简单设计开始,考虑当前的需要(1或者N的情况),当出现冗余设计(N+1)的时候,才决定是否设计为可扩展的结构(Infinity)。重构(Refactoring)的一种有效做法就是发现冗余代码(N>1),将它们通过某种形式提取出来,以便更多(Infinity)的地方使用。这也是人力、时间、机会成本等有限资源下对“无限”的实效价值观点的表达方法。

其实这条规则在现实生活中也有可参考之处。史蒂芬.茨威格(Stefan Zweigs)在《昨日的世界-一个欧洲人的回忆》中曾提到,他到十月革命后的俄国去旅行时,发现俄国人喜欢批条子、签文件而不是像奥地利人那样通过制定法律来处理事务。在我看来,批条子干的就是一个又一个“N”的事,而制度则可以应付“Infinity”类似的事务,当然现实和人远比这条规则复杂得多。

-------
[1].艾萨克.阿西莫夫(Isaac Asimov,192O-1992) ,美籍犹太人,为本世纪最顶尖的科幻小说家之一。“基地”、“机器人” 等系列是艾西莫夫最脍炙人口的代表作。2004的科幻电影"I, Robot"即改编自阿西莫夫的机器人系列。