框架设计原则和规范(四)

时间:2022-05-03
本文章向大家介绍框架设计原则和规范(四),主要内容包括一、 使用规范、2. 修饰属性、3. 集合、4. DateTimeDateTimeOffset、5. ICloneable、6. IComparable/IEquatable、7. IDisposable、8.Nullable<T>、9.Object、10. 序列化、11. Uri、12. System.Xml的使用、13. 相等性操作符、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

祝大家平安夜平安,圣诞节快乐!

此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,将分4天进行推送,今天推送最后两章。

1. 什么是好的框架

2. 框架设计原则

3. 命名规范

4. 类型设计规范

5. 成员设计规范

6. 扩展性设计

7. 异常

8. 使用规范

9. 设计模式

一、 使用规范

1. 数组

1) 要在公共API中优先使用集合,避免使用数组。

2) 不要使用只读(readonly)的数组字段。这种数组用户仍然可以修改数组中的元素

3) 考虑使用不规则数组(jagged array),而不要使用多维数组

2. 修饰属性

Attribute

1) 要在命名自定义修饰属性类时添加“Attribute”后缀。

2) 要在定义自己的修饰属性时使用AttributeUsageAttribute

定义自定义属性的“用法”

[AttributeUsage(...)]

public class ObsoleteAttribute { }

3) 要将可选参数定义为可读写的属性

public class NameAttribute :Attribute {
 ...
 publicint Age { get {...} set {...} } //可选参数
}

4) 要将必填参数定义为只读属性

public class NameAttribute :Attribute {
 ...
 publicint Age { get { ... } set { ... } } //可选参数
}

5) 要提供构造函数参数来对必填参数来进行初始化。每个参数的名字应该与对应属性的名字相同(但大小写会不同)

[AttributeUsage(...)]
public class NameAttribute :Attribute {
 publicNameAttribute(string userName) { ... } //必填属性初始化-UserName
 publicstring UserName { get {...} } //必填属性只读
 ...
}

6) 避免提供构造函数参数来对可选属性(可选参数)进行初始化。

7) 避免对自定义修饰属性的构造函数进行重载

8) 要尽可能将自定义修饰属性类密封起来。这样会对修饰属性的查找更快。

3. 集合

要求你所需要的最弱的类型,并返回你能提供的最强类型

1) 不要在公共API中使用弱类型集合

2) 不要在公共API中使用ArrayList或List<T>

设计用于内部实现而非API接口
//坏设计
public class Order {
 publicList<OrderItem> Items { get {...} }
 ...
}
//好设计
public class Order {
 publicCollection<OrderItem> Items { get {...} }
 ...
}

3) 不要在公共API中使用Hashtable或Dictionary<TKey,TValue>

设计用于内部实现而非API接口

应该使用IDictionary、IDictionary<TKey,TValue>或任何实现了以上两个接口或其中之一的自定义类型

4) 不要使用IEnumerator<T>、IEnumerator或实现了这两个接口之一的任何类型,除非是作为GetEnumerator方法的返回类型

如果你不用GetEnumerator方法来返回枚举器(enumerator),那么这个类就无法用在foreach语句中

5) 不要在同一个类型中同事实现IEnumerator<T>和IEnumerable<T>。对非泛型接口IEnumerator和Enumerable来说也同样如此

类型要么应该是集合,要么应该是个枚举器,但不能两者都是。

6) 集合参数

A.要用最泛的类型来作为参数类型。大多数以集合为参数的成员都使用IEnumerable<T>接口

B. 避免仅仅为了使用Count属性而使用ICollection<T>或ICollection,来做参数

7) 集合属性与返回值

集合作为属性的getter返回值,和方法的返回值

A. 不要提供可设置的集合属性

如果需要替换整个集合,应该考虑提供一个AddRange方法

//坏设计
public class Order {
     publicCollection<OrderItem> Items { get { ...} set {... } }
     ...
}
//好设计
public class Order {
     publicCollection<OrderItem> Items { get {...} }
   ...
}

B. 要用Collection<T>或其子类——如果属性或返回值表示可读写的集合

C. 要用ReadOnlyCollection<T>或其子类,在少数情况下用IEnumerable<T>,如果属性或返回值表示只读属性

D.考虑使用泛型集合基类的子类,而不要直接使用该集合

自定义的集合类型可以有更好的命名,而且可以添加辅助成员。这尤其适用于高级API

E.考虑用Collection<T>或ReadOnlyCollection<T>的子类作为常用方法和常用属性的返回值。

F. 考虑使用有健集合(keyed collection)——如果集合中存储的元素都有独一无二的键值(名字、ID等)。

实现时让它们派生自KeyedCollection<TKey,TItem>

G. 不要从集合属性或以集合为返回值的方法中返回null。而要返回一个空集合或空数组

H.快照集合(Snapshot Collection)和实况集合(Live Collection)

表示某个时间点状态的集合——快照集合

始终表示当前状态的集合——实况集合

a) 不要让属性返回快照集合,属性应该返回实况集合
b) 要用快照集合或实况的IEnumerable<T>(或其子类)来表示不稳定的集合

8) 数组与集合之间的选择

A.优先使用集合,而不是优先使用数组

9) 自定义集合的实现

A.要在设计新的集合时实现IEnumerable<T>

B. 考虑实现非泛型集合(IList/ICollection)接口——如果经常需要把集合传给以这些参数为输入的API。

public class OrderCollection :ILIst<Order>, IList {
     ...
}

C.避免为类型实现集合接口——如果类型的API很复杂,而且与集合的概念无关

D.不要继承自非泛型的集合基类,比如CollectionBase。要使用Collection<T>等

E. 自定义集合的命名

a) 如果实现了IDictionary接口要添加""Dictionary""后缀
b) 如果实现了IEnumerable,并且类型表示的是一个元素列表,要添加“Collection”后缀
c) 要在命名自定义的数据结构时,使用合适的数据结构名。如LinkedList<T>, Stack<T>
d) 避免在为集合抽象命名时添加代表其具体实现的后缀,比如“LinkedList”或“Hashtable”
e) 考虑用集合元素的类型名字做集合名字的前缀。

存储Address类型元素的集合:AddressCollection

XXXDisposableCollection :IDisposable

f) 考虑给只读集合的名字加“ReadOnly”前缀

4. DateTimeDateTimeOffset

1) 如果想要表示一个精确的时间点,要使用DateTimeOffset

2) 要在任何不适合使用绝对时间点的情况下使用DateTime,比如能适用于不同时区的商店开门时间

3) 要在不知道时区或有时候不知道时区的情况下使用DateTime

4) 能用DateTimeOffset就不要用DateTimeKind

5) 要用DateTime来表示所有的日期(比如生日),并将时间部分设置为00:00:00。不要用dateTimeOffset来表示日期。

6) 要用TimeSpan来表示没有日期的时间

5. ICloneable

由于此接口没有指明契约表示深度复制还是浅度复制,所以不要实现它

1) 不要实现ICloneable

2) 不要在公共API中使用ICloneable

3) 考虑为需要克隆几只的类型定义Clone方法。一定要在文档中明确说明该方法执行的是深复制还是浅复制

6. IComparable/IEquatable

1) 要为值类型实现IEquatable<T>

值类型的Object.Equals方法会导致装箱操作,而且默认实现使用了反射,所以效率不高。IEquatable<T>可提供好得多的性能。

2) 要在实现IEquatable<T>.Equals时,同样遵循为覆盖Object.Equals而制定的规范

参见: Object.Equals

3) 要在实现IEquatable<T>的同时覆盖Object.Equals

4) 考虑在实现IEquatable<T>的同时重载operator == 和operator !=

5) 实现IComarable<T>的同时,要实现IEquatable<T>

6) 考虑在实现IComparable<T>的同时重载比较操作符(<、>、<=、>=)

7. IDisposable

参见: Dispose模式

8.Nullable<T>

.NET 2.0新增。表示那些可以为""null“的值类型

1) 考虑用来表示那些可能不存在的值(比如可选的值)

2) 除非在类似的情况下,你会因为,引用类型可以为null,而考虑用引用类型来代替它,不要使用Nullable<T>

例如,不应该用null来表示可选参数

3) 避免用Nullable<bool>来表示通用的具有三种状态的值。

Nullable<bool>应该只用来表示真正可选的布尔值:true,false以及不可用。如果想表示三种状态的值,如yes,no, cancel,考虑使用枚举。

9.Object

1) Object.Equals

参见: 要在实现IEquatable<T>.Equals时,同样遵循为覆盖Object.Equals而制定的规范

A. 覆盖时要遵循的契约:

a) x.Equals(x)返回true
b) x.Equals(y)的返回值与y.Equals(x)相同
c) 如果(x.Equals(y)&& y.Equals(z)) 返回true,那么x.Equals(z)也应该返回true
d) 如果对象x和对象y未被修改,那么连续调用x.Equals(y)应该返回相同的值
e) x.Equals(null)应该返回false

B. 要在覆盖Equals方法同时覆盖GetHashCode方法

C. 考虑在覆盖Object.Equals方法的同时实现IEquatable<T>接口

D.不要从Equals方法中抛出异常

E. 值类型的Equals

a) 要覆盖值类型的Equals方法
b) 要通过实现IEquatable<T>来提供一个以该值类型本身为参数的Equals重载方法。

这样不用装箱

F.引用类型的Equals

a) 考虑覆盖Equals以提供相等语义——如果引用类型表示的是一个值。
b) 不要为可变的引用类型实现“值相等”语义

2) Object.GetHashCode

A.覆盖了Object.Equals就要覆盖GetHashCode方法

B. 确保任何两个对象,如果Object.Equals返回true,那么它们的GetHashCode方法返回值也相同

C. 要竭尽所能的让GetHashCode方法产生随机分布的散列码

D.要确保无论怎么更改对象,GetHashCode都返回完全相同的值

E. 避免在GetHashCode方法中抛出异常

3) Object.ToString

A.要覆盖ToString方法——只要能返回既有用,又易于让人阅读的字符串

开发人员是查看返回的字符串的人员。调试器会在默认情况下用它来显示对象,这非常有价值。

B. 要返回尽量短小的字符串

C. 考虑为每个实例返回独一无二的字符串

D.要使用易于阅读的名字,而不要使用让人无法理解的ID

E. 要在返回和区域性(culture)有关的信息时,根据当前线程的区域性来对字符串进行格式化

F. 要提供重载方法ToString(string format)或实现IFormattable接口——如果ToString()返回的字符串和区域性有关,或者有多重方式来对字符串进行格式化。

例如DateTime

G. 不要返回空字符串或者null

H.避免抛出异常

I. 要确保ToString不会产生副作用

调试器会调用此方法,如果有副作用会增加调试的难度

J. 注意返回的信息中包含的安全性信息,要么获得许可,要么过滤掉

K. 考虑让ToString输出的字符串能为该类型的解析方法正确的解析

DateTime now = DateTime.Now;

DateTime parsed = DateTime.Parse(now.ToString());//解析自己的ToString()

10. 序列化

1) 要在设计新类型时考虑到序列化

2) 选择要支持的序列化技术

A.考虑让类型支持数据协定序列化——如果需要在Web服务中使用该类型,或者需要在Web服务中对该服务进行持久化

参见: 对数据协定序列化的支持

B. 考虑让类型只支持XML序列化,或同时支持数据协定序列化和XML序列化——如果需要在序列化类型是对生成的XML的格式有更多的控制

参见: 对XML序列化的支持

C.考虑让类型支持运行时序列化——如果需要跨越.NET Remoting的边界传输类型

参见: 对运行时序列化的支持

D. 不要仅仅为了进行一般的持久化而支持XML序列化或运行时序列化。应该优先支持数据协定序列化。

3) 对数据协定序列化的支持

参见: 考虑让类型支持数据协定序列化——如果需要在Web服务中使用该类型,或者需要在Web服务中对该服务进行持久化

[DataContract]
class Person {
 [DataMember]string lastName;
 [DataMember]string firstName;
 publicPersion(string firstName, String lastName) { ... }
 publicstring LastName {
          get{ return lastName; }
 }
 publicstring FirstName {
          get{ return firstName; }
          }       
}

A. 考虑将类型中的成员定义为公有的——如果类型会被用于不完全可信的环境

完全可信(full trust)环境中,会对非公有和公有的都进行序列化和反序列化。但在不完全可信环境中,数据协定序列化程序只对公有成员进行序列化和反序列化。

B. 要为所有应用了dataMemberAttribute的属性实现getter和setter.

C. 要用序列化回调函数来对反序列的实例进行初始化

反序列化不会调用构造函数,对于非DataMember标记的字段要特别注意

D. 考虑使用KnowTypeAttribute来表示那些在反序列化复杂的对象图时应该会用到的具体类型

E. 要考虑向前和向后的兼容性

F. 考虑为了支持老版本的双向转换而实现IExtensibleDataObject

4) 对XML序列化的支持

参见: 考虑让类型只支持XML序列化,或同时支持数据协定序列化和XML序列化——如果需要在序列化类型是对生成的XML的格式有更多的控制

A. 避免设计类型时特别考虑XML序列化,除非有强烈的理由要对生成的XML内容加以控制

B. 考虑实现IXmlSerializable接口——如果应用XML序列化修饰属性后生成的XML内容还不能满足需要

5) 对运行时序列化的支持

参见: 考虑让类型支持运行时序列化——如果需要跨越.NETRemoting的边界传输类型

A. 如果类型会被用于.NET Remoting,考虑支持运行时序列化

B. 如果想要完全控制序列化整个过程,考虑实现运行时序列化模式

C. 要将序列化构造函数定义为受保护的,并提供两个参数,如下:

[Serializable]
public class Person : ISerializable{
     protectedPerson (SerializationInfo info, StreamingContext context) {
     ...
     }
}

D. 要显式的实现ISerializable接口的成员

11. Uri

1) 用System.Uri来表示URI和URL数据

2) 考虑为最常用的带System.Uri参数的成员提供基于字符串的重载成员

3) 不要不加思索的为所有基于System.Uri的成员提供基于字符串的重载成员

4) System.Uri实现规范

A.要调用基于System.Uri的重载成员,如果有的话

B. 不要在字符串中存储URI/URL数据

12. System.Xml的使用

1) 不要用XmlNode或XmlDocument来表示XML数据。要尽量使用IXpathNavigable/XmlReader/XmlWrite/XNode的子类型。

2) 要在接受XML或返回XML成员中,以XmlReader,IXpathNavigable或XNode的子类型为输入或输出

3) 不要从XmlDocument派生子类

13. 相等性操作符

1) 不要只重载相等性操作符中的一个

2) 要确保Object.Equals与相等性操作符具有完全相同的语义及相近的性能

3) 避免抛出异常

4) 值类型的相等性操作符

A.要重载值类型的相等性操作符——如果相等性是有意义的

5) 引用类型的相等性操作符

A.避免重载可变引用类型的相等性操作符

B. 避免重载引用类型的相等性操作符——如果其实现会比引用相等性的实现慢得多

感谢大家的阅读,如觉得此文对你有那么一丁点的作用,麻烦动动手指转发或分享至朋友圈。如有不同意见,欢迎后台留言探讨。