.NET设计规范(三) 类型设计规范

第3章 类型设计规范

类型从逻辑上分组为。

ü 要确保每个类型由一组定义明确、相互关联的成员组成。

3.1. 类型和名字空间

ü 要用命名空间把类型组织成一个相关的特性域的层次结构。

û 避免非常深的名字空间层次。这样的层次难于浏览,因为用户不得不经常地回溯。

û 避免有太多的名字空间。

û 避免把为高级场景而设计的类型和为常见编程任务而设计的类型放在同一个名字空间中。

û 不要不指定名字空间就定义类型。

3.2. 类和结构之间的选择

ü 要深入了解引用类型和值类型在行为上的差异。

ü 作为一条经验法则,框架中的大多数类型应该是类。但是在有些情况下,由于值类型所具备的特征,使用结构会更为合适。

ü如果该类型的实例比较小而且生命期比较短,或者经常被内嵌在其他对象中,考虑定义结构而不要定义类。

û 不要定义结构,除非该类型具有以下所有特征:

Ø 它在逻辑上代表一个独立的值,与基本类型(intdouble等等)相似。

Ø 它的实例的大小小于16个字节。

Ø 它是不可变的。

Ø 它不需要经常被装箱。

在所有其他情况下,应该将类型定义为类。

3.3. 类和接口之间的选择

ü 要优先采用类而不是接口。

说明:与基于接口的API相比,基于类的API容易演化得多,因为可以给类添加成员而不会破坏已有的代码。

ü 要用抽象类而不是用接口来解除协定与实现之间的耦合。

说明:抽象类经过正确的设计,同样能够解除协定与实现之间的耦合,与接口所能达到的程度不相上下。

ü 如果需要提供一个多态的值类型层次结构的话,要定义接口。

说明:值类型不能来自其他类型继承,但它们可以实现接口。例如,IComparableIFormat table以及IConvertible都是接口,因此Int32Int64等值类型及其他基本类型,都可以是

comparableformattableconvertible的。

public struct Int32 : IComparable,IFormattable,IConvertible{}

public struct Int64 : IComparable,IConvertible{}

ü 考虑通过定义接口来达到与多重继承相类似的效果。

3.4. 抽象类的设计

û 不要在抽象类型中定义公有的或内部受保护的( protected-internal)构造函数。

说明:

只有当用户需要创建一个类型的实例时,该类型的构造函数才应该是公有的。由于你

无法创建一个抽象类型的实例,因此如果抽象类型具有公有构造函数,那么这样的设

计不仅是不当的,而且还会误导用户。

// bad design

public abstract class Claim

{

public Claim(int number){}

}

// good design

public abstract class Claim

{

// incorrect Design

protected Claim(int number){}

}

ü 要为抽象类定义受保护的构造函数或内部构造函数。

说明:受保护的构造函数更为常见,它仅仅是允许当子类型被创建时,基类能够做自己的初始化。

public abstract class Claim

{

protected Claim() { }

}

内部构造函数可以用来把该抽象类的具体实现限制在定义该抽象类的程序集中。

public abstract class Claim

{

internal Claim() { }

}

ü 要为你发布的抽象类提供至少一个继承自该类的具体类型。

说明:这有助于验证该抽象类的设计是否正确。例如,System.IOFileStreamSystem.IOStream抽象类的一个实现。

3.5. 静态类的设计

ü 要尽量少用静态类。

û 不要把静态类当做杂物箱。每一个静态类都应该有其明确的目的。

û 不要在静态类中声明或覆盖实例成员。

ü 要把静态类定义为密封的、抽象的,并添加一个私有的实例构造函数——如果你的编程语言没有内置对静态类的支持。

3.6. 接口的设计

ü 要定义接口,如果你需要包括值类型在内的一组类型支持一些公共的API

ü 考虑定义接口,如果你需要让已经自其他类型继承的类型支持该接口提供的功能。

û 避免使用记号接口(没有成员的接口)。如果你需要给一个具备某特征(记号)的类做记号,一般来说,最好使用自定义attribute而不要使用接口。

ü 要为接口提供至少—个实现该接口的类型。

例如:这有助于验证接口的设计。例如,System.Collections.ArrayListSystem.Collections.IList接口的一个实现。

ü 要为你定义的每个接口提供至少一个使用该接口的API(一个以该接口为参数的方法,或是一个类型为该接口的属性)。这有助于有助于验证接口的设计。

例如:List<T>.Sort使用了IComparer<T>接口。

û 不要给已经发行的接口再添加成员。

说明:这样做会破坏该接口的实现。为了避免版本的问题,应该创建一个新的接口。

3.7. 结构的设计

û 不要为结构提供默认的构造函数。

ü 要确保当所有的实例数据都为零、falsenull(如果合适)时,结构仍处于有效状态。这可以防止在创建一个结构的数组时创建出无效的实例。

例如:下面的结构设计得不正确。带参数的构造函数有意用来确保状态有效,但是在创建该结构的数组时,这个构造函数没有被执行,因此所有的实例字段都被初始化为0,而对该类型来说这并不是一个有效的状态。

//bad Design

public struct PositiveInteger

{

int value;

public PositiveInteger(int value)

{

if (value <= 0)

{ throw new ArgumentException(); }

this.value = value;

}

public override string ToString()

{

return value.ToString();

}

}

通过确保默认的状态(本例中的value宇段为0)是该类型的有效逻辑状态,这个问题

可以得到解决。

//good Design

public struct PositiveInteger

{

int value;

public PositiveInteger(int value)

{

if (value <= 0)

{ throw new ArgumentException(); }

this.value = value - 1;

}

public override string ToString()

{

return (value + 1).ToString();

}

}

ü 要为值类型实现IEquatable<T>

说明:值类型的ObjectEquals方法会导致装箱,而且它的默认实现也并不非常高效,因为它使用了反射。IEquatable<T>Equals的性能要好得多,而且能够实现为不会导致装箱。

û 不要显式地扩展System.ValueType,事实上大多数编程语言不允许这样做。

3.8. 枚举的设计

ü 要用枚举来加强那些表示值的集合的参数、属性以及返回值的类型性。

ü 要有限使用枚举而不要使用静态常量。

//Avoid the following

public static class Color

{

public static int Red = 0;

public static int Green = 1;

public static int Blue = 2;

}

//Favor the following

public enum Color

{

Red,

Grean,

Blue

}

û 不要把枚举用于开放的集合(比如操作系统的版本、朋友的名字等)

û 不要提供为了今后使用而保留的枚举值。

û 避免显式暴露只有一个值的枚举。

û 不要把sentinel值包含在枚举值中。

例如,下面的代码显示了一个带sentinel值的枚举,这个附加的sentinel值用来表示枚举的最后一个值,其目的是用于范围检查。在框架设计中,这是不好的做法。

public enum DeskType

{

Circular = 1,

Oblong = 2,

Rectangular = 3,

LastValue = 3 //this sentinel should not be here

}

public void OrderDesk(DeskType desk)

{

if (desk > DeskType.LastValue)

{ throw new ArgumentOutOfRangeException(); }

}

ü 框架的开发人员应该用真实的枚举值来执行检查,而不应该依赖于sentinel值。

public void OrderDesk(DeskType desk)

{

if (desk > DeskType.Rectangular||desk<DeskType.Circular)

{ throw new ArgumentOutOfRangeException(); }

}

ü 要为简单枚举类型提供零值。

例如:可以考虑把该值称为“None”之类的东西。如果这样的值不适用于某个特定的枚举那么应该把该枚举中最常用的默认值赋值为零。

public enum Compression

{

None = 0,

GZip,

Deflate

}

public enum EventType

{

Error = 0,

Warning,

Information

}

ü 考虑用Int32(大多数编程语言的默认选择)作为枚举的基本实现类型,除非下面的

任何一条成立:

Ø 该枚举是一个标记枚举,而你有超过32个标记,或预计今后会有更多的标记。

Ø 为了更方便地与需要不同大小的枚举的非托管代码进行互操作,基本的实现类型必须是Int32之外的类型。

Ø 更小的底层实现类型可能会节省相当的空间。

ü 要用复数名词或名词短语来命名标记枚举,用单数名词或名词短语来命名简单枚举。

û 不要直接扩充System.Enum

说明:SystemEnurn是一个特殊的类型,被CLR用来创建用户定义的枚举。大多数编程语言提供了相应的编程元素让你访问这项功能。例如,在C#中,enum关键字被用来定义一个枚举。

3.8.1. 标记枚举的设计

ü 要对标记枚举使用System.FlagsAttribute。不要把该attribute用于简单枚举。

[Flags]

public enum AttributeTargets

{ }

ü 要用2的幂次方作为标记枚举的值,这样就可以通过按位或操作自由地组合它们。

[Flags]

public enum WatcherChangeTypes

{

Created = 0x0002,

Deleted = 0x0004,

Changed = 0x0008,

Renamed = 0xoo10

}

ü 考虑为常用的标记组合提供特殊的枚举值

ü 位操作是一个高级概念,对简单的任务来说应该不是必需的。FileAccessReadWrite

就是这样的一个例子。

[Flags]

public enum FileAccess

{

Read = 1,

Write = 2,

ReadWrite = Read | Write

}

û 避免让创建的标记枚举包含某些无效的组合。

û避免把零用作标记枚举的值,除非该值表示“所有标记都被清除”,而且按下一条规范进行适当的命名。

例如:下面显示了一个常见的实现方法,程序员们用它来判断一个标记是否被设置(见下面的if语句)。该检测对标记枚举所有的值(除了枚举值零)都能够如预期的那样工作,在枚举值为零情况下该布尔表达式的值始终为真。

[Flags]

public enum SomeFlag

{

ValueA = 0,//this might be confusing to users

ValueB = 1,

ValueC = 2,

ValueBAndC = ValueB | ValueC

}

SomeFlag flags = GetValue();

if((flags&SomeFlag.ValueA)==SomeFlag.ValueA)

{}

ü 要把标记枚举的零值命名为None。对标记枚举来说,该值必须始终意味着“所有标记均被清除”。

public enum BorderStyle

{

Fixed3D = Ox1,

FixedSingle = Ox2,

None = Ox0

}

if (foo.BorderStyle == BorderStyle.None)

{}

3.8.2. 给枚举添加值

ü 考虑给枚举添加值,尽管有那么一点兼容性的风险。

说明:如果你有实际数据,表明给枚举添加值会导致应用程序的不兼容,那么可以考虑添加一个新的API来返回新老枚举值,而老的API(仍返回老的枚举值)建议不再使用。这样就能确保仍然兼容现有的应用程序。

3.9. 嵌套类型

ü 要想让一个类型能够访问外层类型的成员时才使用嵌套类型。

û 不要使用嵌套类型来进行逻辑分组,应该用命名空间来达到此目的。

û 避免公开地暴露嵌套类型。唯一的例外是如果只需在极少数的场景中声明嵌套类型的变量,比如派生子类时,或者其他高级的自定义场景中。

û 不要使用嵌套类型,如果该类型可能会被除了它的外层类型之外的类型引用。

例如:如果一个枚举是某类的一个方法的参数,那么这个枚举不应该被定义为该类的嵌套类型。

û 如果它们需要被客户代码实例化,不要使用嵌套类型。如果一个类型具有公有构造函数,那么它可能不应该被嵌套在其他类型中。

û 不要把嵌套类型定义为接口的成员,许多编程语言不支持这种结构。一般来说,应该尽量少用嵌套类型,而且应该避免将嵌套类型公开暴露给外界。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结