The Elements of C# Style - Design
1. 工程
1.1 别怕做工程
不要试图用代码对包括了所有理论上可行的科学实现进行建模。写成有限制性的代码并非过错,只要你确信这些限制性不会影响产品系列的功用。
1.2 简洁优于优雅
1.3 了解重用的代价
重用是一种奇妙之物,增加依赖性和复杂度等代价也不可小视,这些代价有时足以抵消其助益。
1.4 按约编程
方法时调用双飞之间的契约。契约要求调用者必须遵守方法前置条件,而方法也应返回满足与之相关的后置条件的结果。
在适当的公共方法中,应以异常和断言检查前置条件和后置条件。在方法开始处。其他代码执行前检查前置条件,在方法结尾处。方法返回前检查后置条件。
在从覆盖了超类方法的类派生新类时,必须保留超类方法的前置条件和后置条件。为了确保这一点,可采用模版方法设计模式。公共方法均测试前置条件,调用关联的虚方法,再测试后置条件。子类可能通过覆盖虚方法来覆盖超类中的公共行为。
public class LinkedList<T> { public void Prepend(T t) { //测试前置条件 Debug.Assert(... ) ; DoPrepend( t ); //测试后置条件 Debug.Assert( Object.Equals(this[0],t)); } protected virtual void DoPrepend( T t) { ... } } public class Stack<T> : LinkedList<T> { protected override void DoPrepend(T t) { ... } }
1.5 选用适宜的工程方法
考虑选用一种适合你项目的软件设计方法。各种设计方法总结了最佳实践,避免经常导致软件项目失败的缺陷和沟通不足。例如敏捷软件开发,极限编程等。
1.6 分隔不同的编辑层
分隔不同的编辑层,创建一种更为灵活且易于维护的架构。
2. 类的设计
2.1 让类保持简单
如果确定是否一定要某个方法,就不要添加它。如果另一个方法或方法的组合可能有效完成同样的功能,就不要添加方法。添加方法易,剔除方法难。
2.2 定义派生类,使其可以用在任何可以使用期祖先类的地方
通过覆盖来修改或限制其父类行为的派生类,是对该类一种特化(specialization),但其实例化可能仅能有限地代换祖先类的实例。可能并非在用到父类的所有地方均可使用特化类。
在行为上与祖先类兼容的派生类是一种子类型,其实例可与父类的实例完全代换。实现子类型的派生类并不覆盖其祖先类的行为,它只扩展祖先类的服务。子类型拥有与父类型相同的特性和关联关系。
里氏替代原则
使用引用超类的方法必须能够在不知道子类对象的情况下使用子类对象。
The Liskov Substitution Principle
Methods that use references to superclasses must be able to use objects of subclasses without knowing it.
开放-封闭原则
软件实体,如类、模块、函数等应对扩展方法。但对修改封闭。
The Open-Closed Principle
Software entites,i.e. casses,modules, functions ,and so forth ,should be open for extension ,but closed for modification.
public abstract class Shape { public abstract void Reseize( double scale); } public class Rectangle : Shape { protected double width_; protected double height_; public double Width { get { return width_; } set { width_ = value; } } pubic double Height { get { return height_; } set { height_ = value ; } } public override void Resize ( double scale) { this.Width *= scale; this.Height *= scale; } }
public class Square : Rectangle { public double Size { get { return width_ ; } set { width_ = value ; height_ = value ; } } }
由于正方形的宽和高是相同的,你可能考虑修改Rectangle类。
public class Rectangle : Shape { ... public virtual double Width { get { return width_ ; } set { width_ = value ; } } public virtual double Height { get { return height_ ; } set { height_ = value ; } } } public class Square : Rectangle { public override double Width { get { return width_ ; } set { width_ = value ; height_ = value ; } } public override double Height { get { return height_; } set { width_ = value ; height_ = value ; } } }
尽管问题可以解决,但是,需要修改Rectangle父类。也就是需要修改代码,违背了开放-封闭原则。
public class Canvas { public void DrawShape(Shape shape) { if (shape is Circle) { DrawCircle( shape as Circle) ; } else if (shape is Rectangle) { DrawRectangle(shape as Rectangle); } } public void DrawCircle(Circle circle){...} public void DrawRectangle(Rectanglerectangle){...} }
上例为不好的设计。
public abstract class Shape { ... public abstract void DrawSelf (Canvas canvas) ; ... } public class Circle : Shape { ... public override void DrawSelf(Canvas canvas) {...} ... } public class Canvas { ... public void DrawShapes (IEnumerable<Shape> shapes) { foreach(Shape shape in shapes) { shape.DrawSelf(this); } } ... }
2.3 对于“is-a”关系使用继承,对于“has-a”关系使用包含
例如,卡车有一(has-a)组轮胎,运送冰激凌的卡车是一(is-a)中特殊种类的卡车:
public class Wheel { ... } public class Truck { private Wheel [] wheels_ ; } public class IceCreamTruck : Truck { ... }
2.4 对于“is-a”关系使用抽象基类,对于“实现”关系使用接口。
使用基类指明类之间的“is-a”关系,使用接口指明实现关系。
在类和接口之间做选择时,记住以下几点。
类可以包括字段及方法的默认实现,接口只定义方法签名。
类比接口易于扩展:加入新成员时可以不破坏派生类型,而扩展接口时会破坏实现该接口的既有类型。
类只能有一个基类型,但能实现任意数量的接口
3. 线程安全和并发
3.1 设计可重入的方案
在单线程循环调用或被多线程并发调用时均能工作正常的代码。也就是不要使用静态分配资源。
3.2 只在合适的地方使用线程
同时响应许多事件
提供极高响应能力
尽多处理器之用
3.3 避免不必要的同步
4. 效率
4.1 使用懒惰求值和懒惰初始化
在需要结果之前,不要进行复制的计算。总是在最靠近嵌套边缘的地方执行计算。如有可能,缓存结果。
public class LoanCalculator { //... } public class PersonalFinance { private LoanCalculator loanCalculator_ = null ; public double CalculateIntest () { //使用双重检查模式以防止并发构造的发生 if(loanCalculator_ == null) { if(loanCalculator_ == null) { loanCalculator_ = new LoanCalculator(); } } return loanCalculator_ .CalculateIntest () ; } }
4.2 重用对象以避免再次分配
缓存并重用频繁创建并且声明周期有限的对象
使用访问器而不是构造函数来重新初始化对象。
使用工厂设计模式来封装缓存和重用对象机制。
4.3 最后再优化
优化第一原则:
不要优化。
优化第二原则(只对专家有效)
还是不要优化。
在确认需要优化之前,不要花费时间做优化。
采用80-20 原则:系统中20%的代码使用80%的资源。如果要做优化,确认从那20%代码开始。
4.4 避免创建不必要的对象
在知道自己需要什么之前,避免创建对象。
4.5 让CLR处理垃圾回收