敏捷软件开发宣言

                 我们 正通过亲身实践 以及 帮助他人实践,揭示更好的软件开发方法

                              通过这项工作 ,我们认为:

                                                        人和交互              重于                 过程和工具

          可以工作的软件             重于                面面俱到的文档

                       客户合作               重于                 合同谈判

随时应对变化            重于                 遵循计划

虽然右项也有其价值,但我们认为左项更加重要。

原则

1.       我们最优先要做的是通过尽早地、持续地交付有价值的软件来使客户满意。

2.       我们欢迎需求的变化,即使到了开发后期。敏捷过程能够驾驭变化,为客户创造竞争优势。

3.       经常交付可以工作的软件 ,从几个星期到 几个月,时间间隔越短越好。

4.       在整个项目开发期间,业务人员和开发人员必须朝夕工作在一起。

5.       围绕斗志昂扬的人构建项目。给他们提供所需的环境和支持,并且信任他们能够完成工作。

6.       在团队内部,最有效率也最有效果的信息传达方式,就是面对面的交谈。

7.       可以工作的软件是 进度主要的度量标准。

8.       敏捷过程提倡可持续开发。出资人、开发者和用户应该总是保持稳定的开发速度。

9.       对卓越技术和良好设计的不断追求有助于提高敏捷性。

10.   简单——尽量减少工作量的艺术是至关重要的。

11.   最好的架构、需求和设计都源于自我组织的团队。

12.   每隔一定时间,团队都要总结如何更右效率,然后相应地调整自己的行为。

极限编程实践

   完整团队            用户故事            短交付周期       验收测试            结对编码            测试驱动开发 

   集体所有权       持续集成            可持续的开发速度        开放的工作空间             计划游戏

   简单设计            重构      隐喻

避免设计的臭味

·         僵化性 rigidity ——设计难以改变。

·         脆弱性(fragility)——设计易于遭到破坏。

·         顽固性(immobility)——设计难以重用。

·         粘滞性(viscosity)——难以做正确的事情。

·         不必要的复杂性(needless complexity)——过分设计。

·         不必要的重复(needless repetition)——滥用鼠标进行复制、黏贴。

·         晦涩性(opacity)——混乱的表达。

设计原则

·         单一职责原则(SRP):一个类应该只有一个发生变化的原因。

·         开放封闭原则(OCP):软件实体应该对扩展开放,对修改关闭。

·         Liskov替换原则(LSP):子类型(subtype)必须能够替换掉它的基类型(base type)。

·         依赖倒置原则(DIP):a. 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。b.抽象不应该依赖于细节。细节应该依赖于抽象。

·         接口隔离原则(ISP):不应该强迫客户程序依赖并未使用的方法。

·         DRYDon’t repeat yourself Principle。通过抽取公共部分放置在一个地方避免代码重复。

·         封装变化 Encapsulate what varies)。

·         面向接口编程而不是实现(Code to an interface rather than to an implementation)。

·         优先使用组合而非继承(Favour Composition Over Inheritance)。

包和组件的设计原则

  • 重用-发布等价原则(Reuse-Release Equivalence Principle, REP):重用的粒度就是发布的粒度。
  • 共同重用原则(Common-Reuse Principle, CRP):一个组件中的所有类应该是共同重用的。如果重用了组件中的一个类,那么就要重用组件中的所有类。
  • 共同封闭原则(Common-Closure Principle, CCP):组件中的所有类对于同一种性质的变化应该是共同封闭的。一个变化若对一个封闭的组件产生影响,则对该组件中的 所有类产生影响,二对于其他组件则不造成任何影响。
  • 无环依赖原则(Acycle-Dependencies Principle, ADP):在组件的依赖关系图中不允许存在环。
  • 稳定依赖原则(Stable-Dependencies Principle, SDP):朝着稳定的方向进行依赖。
  • 稳定抽象原则(Stable-Abstraction Principle, SAP):组件的抽象程度应该与其稳定程度一致。 

 


posted @ 2008-12-12 15:13 lemonade 阅读(801) 评论(0) 编辑
     我们知道,对于一个给定的数判断该数是不是质数,很简单,只需要对其开根号,然后循环取模即可:
public bool IsPrime(int number)
        {
            
if (number < 2)
            {
                
return true;
            }
            
else
            {
                
for (int i = 2; i <= Math.Sqrt(number); i++)
                {
                    
if (number % i == 0)
                    {
                        
return false;
                    }
                }
            }
            
return true;
        }

     但是,对于一个给定的整数,怎样计算出小于该整数的所有素数呢?比较常见的是采用Sieve of Eratosthenes算法,它的基本思想是这样:

     由于一个合数总是可以分解成若干个质数的乘积,那么如果把质数(最初只知道2是质数)的倍数都去掉,那么剩下的就是质数了。例如要查找100以内的质数,首先2是质数,把2的倍数去掉;此时3没有被去掉,可认为是质数,所以把3的倍数去掉;再到5,再到7,7之后呢,因为8,9,10刚才都被去掉了,而100以内的任意合数肯定都有一个因子小于10(100的开方,可参考前面判断质数的算法),所以,去掉,2,3,5,7的倍数后剩下的都是质数了。

     具体实现,我们通过设置两个数组,一个bool数组crossedOut,用来标识对应下标的数字是不是质数,如果i是质数,crossedOut[i]=false,否则crossedOut[i]=true。那么划掉i可以表示成crossedOut[i]=true。还有一个int型数组是result,用来存储符合要求的素数。具体实现如下:

public class PrimeGenerator
    {
        
private static bool[] crossedOut;
        
private static int[] result;

        
public static int[] GeneratePrimeNumbers(int maxValue)
        {
            
if (maxValue < 2)
            {
                
return new int[0];
            }
            
else
            {
                UncrossIntegersUpTo(maxValue);
                CrossOutMultiples();
                PutUncrossedIntegersIntoResult();
                
return result;
            }
        }

        
private static void UncrossIntegersUpTo(int maxValue)
        {
            crossedOut 
= new bool[maxValue + 1];
            
for (int i = 2; i < crossedOut.Length; i++)
            {
                crossedOut[i] 
= false;
            }
        }

        
private static void PutUncrossedIntegersIntoResult()
        {
            result 
= new int[NumberOfUncrossedIntegers()];
            
for (int j = 0, i = 2; i < crossedOut.Length; i++)
            {
                
if(NotCrossed(i))
                {
                    result[j
++= i;
                }
            }
        }

        
private static int NumberOfUncrossedIntegers()
        {
            
int count = 0;
            
for (int i = 2; i < crossedOut.Length; i++)
            {
                
if (NotCrossed(i))
                {
                    count
++;
                }
            }
            
return count;
        }

        
private static void CrossOutMultiples()
        {
            
int limit = DetermineIterationLimit();
            
for (int i = 2; i <= limit; i++)
            {
                
if (NotCrossed(i))
                {
                    CrossOutputMultiplesOf(i);
                }
            }
        }

        
private static int DetermineIterationLimit()
        {
            
double iterationLimit = Math.Sqrt(crossedOut.Length);
            
return (int)iterationLimit;
        }

        
private static void CrossOutputMultiplesOf(int i)
        {
            
for (int multiple = 2 * i; multiple < crossedOut.Length; multiple += i)
            {
                crossedOut[multiple] 
= true;
            }
        }

        
private static bool NotCrossed(int i)
        {
            
return crossedOut[i] == false;
        }    
    }


 

posted @ 2008-12-10 17:09 lemonade 阅读(1181) 评论(7) 编辑

前言

     上一篇笔者已经讲述了Object中有关ToString()、Equals()、GetHashCode()方法的基本运用,本章将简单介绍另外三个方法:GetType()、MemberwiseClone()、Finalize(),希望在本章完了以后大家对Object对象有一个深刻详细的认识。

应用

4 GetType(): 获取元数据

     提到GetType()方法,就不得不引入反射。在.NET中,反射是一个运行库类型发现的过程。通过反射,可以得到一个给定程序集所包含的所有类型的列表,这个列表包括给定类型中定义的方法、字段、属性和事件。也可以动态地发现给定类(或结构)支持的接口、方法的参数和其他相关细节(基类、命名空间、清单数据等)。对于反射,这里就不多做介绍。GetType()方法,返回的是一个System.Type类型,一旦获取了类型的Type信息(实质上是类型元数据)我们就相当于得到了该类型的所有信息,包括接口定义,方法参数等等。一般来说有三种方式得到类型的Type信息:

  1. 使用System.Object定义的GetType()方法,它返回一个表示当前对象元数据的Type信息。要使用这个方法,必须得到类型的编译时信息,并且需要先建立一个实例。
  2. 使用System.Type.GetType()得到Type引用,里面包含三个非常有用的静态函数,可通过传入类型的完全限定名来获取Type信息。
  3. 最后一个获取类型信息的方法是使用C# typeof运算符运算符。类似Type.GetType(),使用typeof运算符,我们不需要先建立一个实例来提取类型信息。但是仍然需要知道类型的编译时信息。

     第一种方法的局限性在于必须先创建类型的实例才能提取类型信息,也就是说类型是在编译时检查的;第三种方法,同样需要知道类型的编译时信息;第二种相对来说最灵活,只需要通过传入类型完全限定名的字符串就能提取类型信息。本文旨在讨论Object的GetType方法,下面给出一段代码示例:

Code

     代码依旧是在之前的基础上改动,前面已经演示了ToString()方法,只不过现在我们是通过反射来调用ToString()方法,运行前需要引用System.Reflection命名空间,代码比较简单,这里就不作解释了,运行结果如下:

     ErrorCode = 1;ErrorString = error string

     System.Type类定义了大量成员,可以用来检查一个类型的元数据,调用属性、方法,获取Attribute信息等,大家可以查API熟悉一下,只要记住一点:反射的实质是获取元数据。

5 MemberwiseClone(): 浅复制

     MemberwiseClone()方法返回一个新的对象,它是当前对象的逐个成员的副本。因此,如果你的对象包含到其他对象的引用,那么到这些类型的引用将被复制(也就是说,它实现了浅复制)。如果对象只包含值类型,得到的是值的完全副本。需要注意的是,MemberwiseClone方法是proteced,故对象的用户无法直接调用这个方法,而一个对象可能在克隆过程中自己调用这个方法。如果我们在Main()方法中写类似的代码:Status status = new Status(1, "error string"); Status newStatus = (Status )status.MemberwiseClone(); 是不能通过编译的,因为由于MemberwiseClone()方法的访问级别限制,你只能在Status类里面通过this.MemberwiseClone()调用该方法,而不能在客户代码里面直接调用。

     一般来说,我们主要用它来实现Clone方法,如果你想使自己的自定义类型支持向调用方法返回自身同样副本的能力,需要实现标准ICloneable接口。这个类型定义了一个简单的方法Clone(),接口定义如下:

ICloneable

     很明显,不同对象的Clone()方法实现不一样。但基本功能差不多,都是将成员变量的值复制到新的对象实例,然后向用户返回该实例。现在我们对Status类实现ICloneable接口,实现代码如下:

Clone

     当然,有时候为了简单起见,我们可以直接调用MemberwiseClone()方法来完成这样的功能,代码示例如下:

Clone

     很显然,这样更简单,但是有个前提:克隆对象包含的类型都是值类型,如果里面包含引用类型,由于MemberwiseClone()是浅复制,引用类型只会复制对象的引用。有个折中的方式是,我们可以先调用this.MemberwiseClone()方法克隆值类型,然后通过new创建引用类型对象。假设Status类里面还有个StateDescription的引用类型变量,那么我们要想实现深度复制,就得这样写代码了:

Code

 6 Finalize(): 资源清理

     当为自定义类重写Finalize()时,就建立了一个地方,存放为类型执行必要的清理逻辑。因为这个成员被定义为受保护的,所以不可能直接调用一个对象的Finalize()方法。相反,在从内存删除这个对象之前,垃圾回收器会调用对象的Finalize()方法。

     当然,Finalize()的调用将(最终)发生在一次自然的垃圾回收或用程序通过GC.Collect()强制回收过程中。另外,当承载应用程序的应用程序域从内存中卸载时,会自动调用类型的终结器方法。大多数的C#类都不需要显示的清理逻辑。原因很简单:如果类型使用了其他托管对象,一切都最终会被垃圾回收。只是在你使用非托管资源时,才可能需要设计一个在用完后清理自身的类。需要注意的是,在结构类型上重写Finalize()是不合法的。这一点非常重要,因为结构是值类型,它们本来就从不分配在堆上。

     在C#中,不能通过override关键字重写Finalize()方法,当想配置自定义的C#类类型来重写Finalize()方法时,可以使用下面的析构函数语法来达到同样的效果。之所以要用这种重写虚函数的替代形式,是因为当C#编译器执行一个构造函数时,它将自动在Finalize()方法中增加许多必需的基础代码。假设需要在Status类中重写Finalize()方法,代码如下:

 ~Status()
 {
    
//清除这里非托管的资源
 }       

     要记住,Finalize()方法的作用是保证.NET对象能在垃圾回收时清除非托管资源。如果创建了一个不使用非托管实体的类型,终结是没有用的。事实上,只要有可能的话,就应该在设计类型时避免提供Finalize()方法,原因很简单,终结是要花费时间的。

     当在托管堆上分配对象时,运行库自动确定该对象是否提供一个自定义的Finalize()方法。如果是这样,对象将被标记为可终结的,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每一个在从堆上删除之前必须被终结的对象。当垃圾回收器确定到了从内存中释放一个对象的时间时,它检查终结队列上的每一个项,并将对象从堆上复制到另一个称作终结可达表的托管结构上。此时,下一个垃圾回收时将产生另一个线程,为每一个在可达表中的对象调用Finalize()方法。因此,为了真正终结一个对象,至少要进行两次垃圾回收。总而言之,尽管对象的终结能够保证对象可以清除非托管的资源,但它本质上仍然是非确定的,而且由于额外的幕后处理,速度会变的很慢。

     除了重写Finalize()方法,其实我们也可以通过另一个更安全的方式处理对象清理工作,即通过实现IDisposable接口,它定义了一个名为Dispose()的方法:

IDisposable

     如果提供IDisposable接口,就是假设当对象的用户不再使用这个对象时,会在这个对象引用离开作用域之前手动调用Dispose()。这样,对象可以执行任何必要的非托管资源的清理,而且不会再有将对象放在终结队列上导致的性能损失,也不必等待垃圾回收器触发类的终结逻辑。需要注意的是,Dispose()方法不只负责释放一个对象的托管资源,还应该对任何它包含的可处置对象调用Dispose()。于Finalize()不一样,在Dispose()方法中与其他托管对象通信是安全的(在Finalize()方法中调用托管对象的话,难以预见该托管对象是否已经被垃圾回收器清理了,固会产生难以预见的情况)。原因很简单:当对象的用户调用这个方法时,对象仍然在托管堆上,并可以访问所有其他分配在堆上的对象。

     可能有人要说,虽然我实现了IDisposable接口,但是如果程序中忘记了调用Dispose()怎么办呢?答案是混合两种模式。微软定义了一个正式的可处置模式,它在健壮性、可维护性和性能三者间取得了平衡,具体实现如下:

Code

     代码示例比较长,但是建议大家仔细看一下,是个很优雅的设计。

总结

     至此,对System.Object的分析就到这了,本文更多的是从应用的层面探讨Object基类,其中又引入了很多与之相关的应用点,如果你完整地阅读了这两篇文章,我对你的耐心表示感谢,也希望你能从中有所收获。

posted @ 2008-12-05 18:19 lemonade 阅读(188) 评论(0) 编辑

引言

     Object类是C#中所有类型的基类,但由于对它的继承是隐式的,故大多数人对它并不太在意,其实object中包含了很多有用的方法,对它有个清晰的了解能够很好地帮你理清楚c# API的层次结构,本文就Object中6个方法进行简单的说明,重点讲述Equals方法。

综述

     在.Net中,每一个类型都继承自一个公共的基类:System.Object。Object类定义了.NET世界中每一个类型都支持的一组公共的成员集合。当创建任何一个不显示指定其基类的类时,它隐含继承自System.Object,当然,你也可以显示地继承。下面来看一下,System.Object的接口:

Code

     可以看出,Object类主要包含四个虚方法,子类可以重写它,应该说这几个是比较常用的(除了Finalize());两个静态方法,object对其有具体实现,可以直接使用;两个实例方法,GetType在反射中用的比较多,MemberwiseClone在实现ICloneable接口时比较常用。至此,相信大家对object应该有了一个整体的认识,下面分别对它的几个方法成员进行描述。

应用

1 ToString() : 自描述

     用一句话来概括ToString()方法的作用,我觉得应该是“提供了一种获得对象当前状态的快照”。object类对它的默认实现是返回该对象的完全限定名,也就是说,如果你定义的类没有重写ToString()方法,那么直接调用当前对象的ToString()方法,返回的是一个字符串:命名空间+类名。如下面的代码,输出的结果是: ConsoleApplication.Program

Code

     上面已经说了,ToString()方法主要用来返回对象当前状态的一个快照,故如果我们在项目中有这种需求,就应该重写该方法。下面写个简单的代码实例,后面几个方法的说明都会围绕这个实例说明。如果做网络协议方面的开发,经常会遇到各种各样的Error Code,下面我们设计一个简单的类,用来描述一个网络操作的返回结果,代码如下:

Code

     Status类包括两个数据成员,errorCode 和 errorString,分别用来描述错误码和错误提示信息,这里是为了简单化,实际的开发中errorCode一般是设计成enum类型的,易于标识。接着,我们在Status类中重写ToString()方法,代码如下:

Code

     代码很简单,就是将当前对象的数据以一定的形式输出来,下面我们写个客户端代码简单测试下:

Code

返回结果如下:

     ErrorCode = 1;ErrorString = ErrorString1
     ErrorCode = 2;ErrorString = ErrorString2

小结:ToString()方法主要用来描述对象的当前状态,在某些特定应用中非常有用,比如在一些网络协议中,我们需要以Http Get的方式发送数据给服务器,那么构建url就是个必须的工作,如果只是简单几个参数可能比较容易构建,但是一旦需要传递的参数很多,那么比较OO的做法就是构建一个类,将那些需要传递的数据作为数据成员封装好,然后重写ToString()方法,将对象的数据按照url要求的格式组织好返回。

2 Equals(): 判等

     Equals相关的方法总共有三个,Equals(Object, Object) : Boolean;  ReferenceEquals(Object, Object) : Boolean; Equals(Object) : Boolean。前两个方法object已经提供了具体的实现,对于ReferenceEquals,顾名思义,是用来判断两个对象的引用是否相同,需要注意两点:

     1 如果传入两个null对象,返回true。

     2 由于ReferenceEquals的两个参数都是Object类型的,如果传入值类型将会进行装箱,由此会导致引用不相同,比如代码:Object.ReferenceEquals(2, 2),返回的是false,因为对2进行装箱以后倒置两者指向了不同的引用。如果这样写呢:int n = 2; Object.ReferenceEquals(n, n); 返回的依然是false。

     接着,简单说明一下Equals(Object, Object) : Boolean,.NET中对它的实现如下:

Code

流程如下:    

    判断两个对象是否指向同一个引用(包括两者为null的情况),如果是则返回true,否则继续进行;

    如果两者都不为null的时候,返回的结果取决于实例方法Equals的返回值。

object对Equals(Object)的默认实现是只有当两个对象指向相同的引用才返回true,下面我们来对该方法进行重载,代码依旧在Status的基础上改,加一个方法,代码如下:

Code

     先判断obj对象是否为null以及它的类型,如果它是一个非空的Status类型则比较对象的值,相等则返回true。修改Main方法,对Equals方法进行测试:

Code

输出结果为True,倘若没有重写Equals方法,输出的将是False,因为两者的状态数据相同,但是指向不同的引用。

重写了Equals方法后,重载 == 和 != 运算符其实也是件很容易的时,直接调Equals方法即可,但是引用《Effective c#》中的建议:

     1 定义==重载函数的时候,也要定义!=重载函数。     

     2 值类型最好不要重载定义Equals函数,而引用类型最好不要重载定义==操作符。

至此,有关Equals就介绍到这,相信大家对它应该有了一个清晰的认识,接着我们来看Object下一个与Equals密切相关的成员方法。

3 GetHashCode(): 对象地址

     GetHashCode()方法返回一个能够标识内存中指定对象的整数,如果你打算将自定义的类型包含进System.Collections.HashTable类型中,强烈建议你重写这个方法的默认实现。当我们重写了Equals(Object)实例方法后,编译器会产生一个警告,建议你同时也重写GetHashCode方法。GetHashCode的作用是返回一个数值,又叫散列码,它根据对象的内部状态数据表识对象。因此,如果两个对象的状态数据相同,也应该获得相同的散列码。一般来说,重写GetHashCode只在打算将自定义的类型保存在一个基于散列值的集合中时有用。在底层,HashTable类型调用所含类型的GetHashCode()以及Equals()成员来确定要返回给调用者的正确对象。

     创建散列码的算法有很多,这里不做说明,System.String类提供了一个可靠的GetHashCode()的实现,它基于字符串的字符数据。如果能确定某个字符串字段在对象之间是唯一的,例如id,那么就可以直接对这个字段的字符串调用GetHashCode();如果找不到这样一个唯一型字段,但已经重写了ToString(),可以直接从它GetHashCode(),对于Status类,由于我们已经实现了ToString()方法,就可以这样简单实现GetHashCode方法,代码如下:

Code

     有关HashCode是否有效的判断标准,引用《Effective C#》中描述如下:  

     1. 如果两个对象相等(由operator==定义),它们必须产生相同的散列码。否则,这样的散列码不能用来查找容器中的对象[22]。

     2. 对于任何一个对象A,A.GetHashCode()必须是一个实例不变式(invariant)。即不管在A上调用什么方法,A.GetHashCode()都必须总是返回相同的值。这可以确保放在“散列桶”中的对象总是位于正确的“散列桶”中。

     3. 对于所有的输入,散列函数应该在所有整数中产生一个随机的分布。这样,我们才能从一个散列容器上获得效率的提升。

     那么如果两个对象不相等能否产生相同的散列码呢? 网上搜了一下,看到有网友发的一段代码,似乎对于不同的状态数据,是可以返回相同的散列码,不信运行下面代码试一下:

Code

返回的结果都是:2060653827

     想要了解更多有关GetHashCode()方法的描述,可以参考《Effective C#: 改善C#程序的50种方法》

posted @ 2008-12-02 22:09 lemonade 阅读(328) 评论(0) 编辑

第四章 关键的“构建”决策 (key Construction Decision)

Checklist: 主要的构建实践 (Major Construction Practices)

编码
  • 你有没有确定,多少设计工作将要预先进行,多少设计工作在键盘上进行(在编写代码的同时)?
  • 你有没有规定诸如名称、注释、代码格式等“编码约定”?
  • 你有没有规定特定的由软件架构确定的编码实践,比如如何处理错误条件、如何处理安全性事项、对于类接口有哪些约定、可冲用的代码遵循那些标准、在编码时考虑多少性能因素等?
  • 你有没有找到自己在技术浪潮中的位置,并相应调整自己的措施?如果必要,你是否知道如何“深入一种语言编程”,而不受限于语言(仅仅“在一种语言上编程”)?
团队工作
  • 你有没有定义一套集成工序——即,你有没有定义一套特定的步骤,规定程序员在把代码check in到主源码(代码库)中之前,必须履行这些步骤?
  • 程序员是结队编程、还是独自编程,或者这两者的某种结合?
质量保证
  • 程序员在编写代码之前,是否先为之编写测试用例?
  • 程序员会为自己的代码写单元测试吗(无论先写还是后写)?
  • 程序员在check in代码之前,会用调试器单步跟踪整个代码流程吗?
  • 程序员在check in代码之前,是否进行集成测试?
  • 程序员会复审(review)或检查别人的代码吗?
工具
  • 你是否选用了某种版本控制工具?
  • 你是否选定了一种语言,以及语言的版本或编译器版本?
  • 你是否选择了某个编程框架(framework, 如J2EE或Microsoft.NET),或者明确地决定不使用编程框架?
  • 你是否决定允许使用非标准的语言特性?
  • 你是否选定并拥有了其他将要用到的工具——编辑器、重构工具、调试器、测试框架、语法检查器等?

Key Points

  • 每种编程语言都有其优点和弱点。要知道你使用的语言的明确优点很弱点。
  • 在开始编程之前,做好一些约定(convention)。如“改变代码使之符合这些约定”是近乎不可能的。
  • “构建的实践方法”的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法。
  • 问问你自己,你采用的编程实践是对你所用的编程语言的正确响应,还是受它的控制?请记得“深入一种语言去编程”,不要仅“在一种语言上编程”。
  • 你在技术浪潮中的位置决定了哪种方法是有效的——甚至是可能用到的。确定你在技术浪潮中的位置,并相应调整计划和预期目标。

 第五章 软件构建中的设计 (Design in Construction)

Checklist: 软件构造中的设计

设计实践
  • 你已经作过多次迭代,并且从众多尝试结果中选择最佳的一种,而不是简单选择第一次尝试的结果吗?
  • 你尝试用多种方案来分解系统,以确定最佳方案吗?
  • 你同时用自下而上和自上而下的方法来解决设计问题吗?
  • 为了解决某些特定的问题,你对系统中的风险部分或者不熟悉的部分创建过原型、写出数量最少的可抛弃的代码吗?
  • 你的设计方案被其他人检查了吗(无论正式与否)?
  • 你一直在展开设计,直到实施细节跃然纸上吗?
  • 你用某种适当的技术——比如说Wiki、电子邮件、挂图、数码照片、UML、CRC卡或者在代码写注释——来保留设计成果吗?
设计目标
  • 你的设计是否充分地处理了由系统架构层定义出并且推迟确定的事项?
  • 你的设计被划分为层次吗?
  • 你对把这一程序分解成为子程序、包和类方式感到满意吗?
  • 类与类之间的交互关系是否已设计为最小化了?
  • 类和子程序是否被设计为能够在其他的系统中重用?
  • 程序是不是易于维护?
  • 设计是否精简?设计出来的每一部分都绝对必要吗?
  • 设计中是否采用了标准的技术?是否避免使用怪异且难以理解的元素?
  • 整体而言,你的设计是否有助于最小化偶然性的和本质性的复杂度吗?

key points

  • 软件的首要技术使命就是管理复杂度。以简单性作为努力目标的设计方案对此最有帮助。
  • 简单性可以通过两种方式来获取:一是减少在同一时间所关注的本质性复杂度的量;二是避免生成不必要的偶然的复杂度。
  • 设计是一种启发式的过程。固执于某一种单一方法会损害创新能力,从而损害你的程序。
  • 好的设计都是迭代的。你尝试设计的可能性越多,你的最终设计方案就会变得越好。
  • 信息隐藏是个非常有价值的概念。通过询问“我应该隐藏什么?”能够解决很多困难的设计问题。

第六章 可以工作的类 (Working Classes)

Checklist: 类的质量 (Class Quality)

抽象数据类型
  • 你是否把程序中的类都看作是抽象数据类型了?是否从这个角度评估它们的接口?
抽象
  • 类是否有一个中心目的?
  • 类的命名是否恰当?其名字是否表达了其中心目的?
  • 类的接口是否展现了一致的抽象?
  • 类的接口是否能让人清楚明白地知道该如何用它?
  • 类的接口是否足够抽象,使你能不必顾虑它是如何实现其服务的?你能把类看作黑盒子吗?
  • 类提供的服务是否足够完整,能让其他类无须动用其内部数据?
  • 是否已从类中除去无关信息?
  • 是否考虑过把类进一步分解为组件类?是否已尽可能将其分解?
  • 在修改类时是否维持了其接口的完整性?
封装
  • 是否把类的成员的可访问性降到了最小?
  • 是否避免暴露类中的数据成员?
  • 在编程语言所许可的范围内,类是否已尽可能地对其他的类隐藏了自己的实现细节?
  • 类是否避免对其使用者,包括派生类会如何使用它做了假设?
  • 类是否不依赖于其他类?它是松散耦合的吗?
继承
  • 继承是否只用来建立“是一个/is a“的关系?也就是说,派生类是否遵循了LSP(Liskov替换原则)?
  • 类的文档中是否记述了其继承策略?
  • 派生类是否避免了“覆盖”不可覆盖的方法?
  • 是否把公用的接口、数据和行为都放到尽可能高的继承层次中了?
  • 继承层次是否很浅?
  • 基类中所有的数据成员是否都被定义为private而非protected的了?
跟实现相关的其他问题
  • 类中是否只有大约七个或更少的数据成员?
  • 是否把类直接或间接调用其他类的子程序的数量减少到最少了?
  • 类是否只在绝对必要时才与其他类相互协作?
  • 是否在构造函数中初始化了所有的数据成员?
  • 除非拥有经过测量的、创建浅层复本的理由,类是否都被设计为当作深层复本使用?
与语言相关的问题
  • 你是否研究过所用编程语言里和类相关的各种特有问题?

key Points

  • 类的接口应提供一致的抽象。很多问题都是由于违背该原则而引起的。
  • 类的接口应该隐藏一些信息——如某个系统接口、某项设计决策、或一些实现细节。
  • 包含往往比继承更为可取——除非你要对“是一个/is a“ 的关系建模。
  • 继承是一种有用的工具,但却会增加复杂度,这有违于软件首要技术使命——管理复杂度。
  • 类是管理复杂度的首选工具。要在设计类时给予足够的关注,才能实现之一目标。
posted @ 2008-12-01 20:58 lemonade 阅读(85) 评论(0) 编辑

摘自《Code Complete》一书,将书中的一些Check List和Key Points列出来,以备遗忘。

第一章 欢迎进入软件构建的世界 (Welcome to Softeware Construction)

key Points:

  • 软件构建是软件开发的核心活动:构建活动是每个项目中唯一一项必不可少的工作。
  • 软件构建的主要活动包括:详细设计、编码、调试、集成、开发者测试(包括单元测试和集成测试)。
  • 构建也常被称作“编码”和“编程”。
  • 构建活动的质量对软件的质量有着实质性的影响。
  • 最后,你对“如何进行构建”的理解程度,决定了你这名程序员的优秀程度。

第二章 用隐喻来更充分地理解软件开发 (Metaphors for a Richer Understanding of Softeware Development)

Key Points:

  • 隐喻是启示而不是算法。因此它们往往有一点随意。
  • 隐喻把软件开发过程与其他你熟悉的活动联系在一起,帮助你更好地理解。
  • 有些隐喻比其他一些隐喻更贴切。
  • 通过把软件的构建过程比作是房屋的建设过程,我们可以发现,仔细的准备是必要的,而大型项目和小型项目之间也是有差异的。
  • 通过把软件开发的实践比作是智慧工具箱的工具,我们又发现,每位程序员都有许多工具,但并不存在任何一个能适合所有工作的工具,因地制宜地选择正确工具是成为能有效编程的程序员的关键。
  • 不同的隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合。

第三章 三思而后行:前期准备 (Measure Twice, Cut Once: Upstream Prerequisites)

Checklist: 需求 (Requirements)

     这张需求核对表包含了一系列的问题——问问自己项目的需求工作做得如何。本书并不会告诉你如何做好需求分析,所以列表里面也不会有这样的问题。在开始构建之前,用这份列表做一次“心智健全”检查,看看你的地基到底有多坚固——用“需求里氏震级”来衡量。

     并不是需求表中所有的问题都使用于你的项目。如果你做的是一个非正式项目,那么你会发现有些东西根本就不需要考虑。你还会发现一些问题你需要考虑,但不需要做出正式回答。如果你在做一个大型的、正式的项目,你也许就要逐条考虑了。

针对功能需求
  • 是否详细定义了系统的全部输入,包括其其来源、精度、取值范围、出现频率等?
  • 是否详细定义了系统的全部输出,包括目的地、精度、取值范围、出现频率、格式等?
  • 是否详细定义了所有输出格式(Web页面、报表、等等)?
  • 是否详细定义了所有硬件及软件的外部接口?
  • 是否详细定义了全部外部通信接口,包括握手协议、纠错协议、通信协议等?
  • 是否列出了用户想要做的全部事情?
  • 是否详细定义了每个任务所用的数据,以及每个任务得到的数据?
针对非功能需求(质量需求)
  • 是否为全部必要的操作,从用户的视角,详细描述了期望响应时间?
  • 是否详细描述了其他与计时有关的考虑,例如处理时间、数据传输率、系统吞吐量?
  • 是否详细定义了安全级别?
  • 是否详细定义了可靠性,包括软件失灵的后果、发生故障时需要保护的至关重要的信息、错误检测与恢复的策略等?
  • 是否详细定义了机器内存和剩余磁盘空间的最小值?
  • 是否详细定义了系统的可维护性,包括适应特定功能的变更、操作环境的变更、与其他软件的接口的变更能力?
  • 是否包含对“成功”的定义?“失败”的定义呢?
需求的质量
  • 需求是用用户的语言书写的吗?用户也这么认为吗?
  • 每条需求都不与其他需求冲突吗?
  • 是否详细定义了相互竞争的特性之间的权衡——例如,健壮型与正确性之间的权衡?
  • 是否避免在需求中规定设计(方案)?
  • 需求是否在详细程度上保持相当一致的水平?有些需求应该更详细地描述吗?有些需求应该更粗略地描述吗?
  • 需求是否足够清晰,即使转交给一个独立的小组去构建,他们也能理解吗?开发者也这么想吗?
  • 每个条款都与待解决的问题及其解决方案相关吗?能从每个条款上溯到它在问题域中对应的根源吗?
  • 是否每条需求都是可测试的?是否可能进行独力的测试,以检查满不满足各项需求?
  • 是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?
需求的完备性
  • 对于在开始开发之前无法获得的信息,是否详细描述了信息不完全的区域?
  • 需求的完备性是否能达到这种程度:如果产品满足所有需求,那么它就是可接受的?
  • 你对全部需求都感到舒服吗?你是否已经去掉了那些不可能实现的需求——那些只为了安抚客户和老板的东西?

Checklist: 架构 (Architecture)

     以下是一份问题列表,优秀的架构应该关注这些问题。这张核对表的意图并非用做一份有关如何做架构的完全指南,而是作为一种实用的评估手段,用来评估软件食物链到了程序员这一头还有多少营养成分。这张核对表可用做你自己的核对表的出发点。就像“需求”的核对表一样,如果你从事的是非正式项目,那么你会发现其中某些条款甚至都不用去想。如果你从事的是更大型的项目,那么大多数条款都会是很有用的。

针对各架构主题:
  • 程序的整体组织结构是否清晰?是否包含一个良好的架构全局观(及其理由)?
  • 是否明确定义了主要的构造块(包括每个构造块的职责范围及与其他构造块的接口)?
  • 是否明显涵盖了“需求”中列出的所有功能(每个功能对应的构造块不太多也不太少)?
  • 是否描述并论证了那些最关键的类?
  • 是否描述并论证了数据设计?
  • 是否描述定义了数据库的组织结构和内容?
  • 是否指出了所用的关键的业务规则,并描述了其对系统的影响?
  • 是否描述了用户界面设计的策略?
  • 是否将用户界面模块化,使界面的变更不会影响程序其余部分?
  • 是否描述并论证了处理I/O的策略?
  • 是否估算了稀缺资源(如线程、数据库连接、句柄、网络带宽等)的使用量,是否描述并论证了资源管理的策略?
  • 是否描述了架构的安全需求?
  • 架构是否为每个类、每个子系统、或每个功能域(functionality area)提出空间与时间预算?
  • 架构是否描述了如何达到可伸缩性?
  • 架构是否关注互操作性?
  • 是否描述了国际化/本地化的策略?
  • 是否提供了一套内聚的错误处理策略?
  • 是否规定了容错的办法(如果需要)?
  • 是否证实了系统各个部分的技术可行性?
  • 是否描述了过度工程的方法?
  • 是否包含了必要的“买 vs. 卖”的决策?
  • 架构是否描述了如何加工被复用的代码,使之符合其他架构目标?
  • 是否将架构设计得能够适应很可能出现的变更?
架构的总体质量
  • 架构是否解决了全部需求?
  • 有没有哪个部分是“过度架构/overarchitected“或“欠架构/underarchitected”?是否明确宣布了在这方面的预期指标?
  • 整体架构是否在概念上协调一致?
  • 顶层设计是否独立于用作实现它的机器和语言?
  • 是否说明了所有主要的决策和动机?
  • 你,作为一名实现该系统的程序员,是否对这个架构感觉良好?

Key Points:

  • 构建活动的准备工作的根本目标在于降低风险。要确认你的准备活动是在降低风险,而非增加风险。
  • 如果你想开发高质量的软件,软件开发过程必须由始至终关注质量。在项目初期关注质量,对产品质量的正面影响比在项目末期关注质量的影响要大。
  • 程序员的一部分工作是教育老板和合作者,告诉他们软件开发过程,包括在开始编程之前进行充分准备的重要性。
  • 你所从事的软件项目的类型对构建活动的前期准备有重大影响——许多项目应该是高度迭代式的,某些应该是序列式的。
  • 如果没有明确的问题定义,那么你可能会在构建期间解决错误的问题。
  • 如果没有做完良好的需求分析工作,你可能没能察觉待解决问题的重要细节。如果需求变更发生在构建之后的阶段,其代价是“在项目早期更改需求”的20至100倍。因此在开始编程之前,你要确认“需求”已经到位了。
  • 如果没有做完良好的架构设计,你可能会在构建期间用错误的方法解决正确的问题。架构变更的代价随着“为错误的架构编写的代码数量”增加而增加,因此,也要确认“架构”已经到位了。
  • 理解项目的前期准备所采用的方法,并相应地选择构建方法。

 

posted @ 2008-12-01 19:47 lemonade 阅读(166) 评论(0) 编辑
摘要: 引言面向对象的三大利器:封装,继承,多态,正是因为这三个主要特性,演化出了众多优秀的设计模式和框架,只有掌握了它们才能真正掌握面向对象。本文主要探讨继承的概念,都是些简单的语法,但是可能很多人在学习了若干框架、模式后,反而忘记或者说淡忘了一些基本的概念,笔者就是其中一个,故写下此文,一方面温故而知新,另一方面也强调基础的重要性。概念继承就是在类之间建立一种相交关系,使得新定义的派生类的实例可以继承已有的基类的特征和能力,而且可以加入新的特性或者是修改已有的特性建立起类的新层次。我们知道,现实中的事物都是有其相似和区别的,如果我们将相似的特征提取出来集中描述,不同的特征分别描述并包括那些相同的特阅读全文
posted @ 2008-11-28 22:30 lemonade 阅读(122) 评论(0) 编辑
摘要: 引言翻看微软的PetShop范例的时候,无意中发现了一段代码,是用来对数据库中不同的表建立缓存依赖,其中很巧妙地运用了继承的概念,翻阅资料,方知叫Template Method 设计模式,于是将一些心得跟体会写下来与大家分享。概念GoF《设计模式》中说道:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这就好比我们建房子,整个架构我们定好了,至于往里面添什么不会影响整体的构造。它主要用来解决在软件构建过程中,对于某一项任务,常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的阅读全文
posted @ 2008-11-28 18:08 lemonade 阅读(233) 评论(2) 编辑
摘要: 1Observer设计模式#regionObserver设计模式2/**//*3*假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;4*在水烧开时由警报器发出警报、显示器显示提示和水温。5*6*Observer设计模式中主要包括如下两类对象:7*(1)Subject:监视对象,它往往包含着其它对象所感兴趣的内容。在本范例中,热水器就是一个监视对象,它包含的其它对象所感兴趣的内容,就是temprature8*字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。9*(2)Observer:监视者,阅读全文
posted @ 2008-11-03 10:15 lemonade 阅读(464) 评论(0) 编辑
摘要: 引言 代理设计模式(Proxy)可能对大多数人来说并不熟悉,然而其在WebService方面的应用却是非常广泛,本文试图通过三个简单的实例描述代理设计模式的应用场景,希望读者通过这篇文章能够对代理设计模式有一个大概的认识。 应用场景 代理设计模式,说简单点就是为目标对象提供一个代理以控制对其的访问。当我们需要使用的对象很复杂或者需要很长的时间去构造,这时,我们就可以使用代理模式(Proxy)。它相当于Client与实际请求对象之间的一个中介,我们可以理解为它是一个房屋中介。 为什么需要中介呢?我们可以考虑下,当我们买房或者租房的时候,为什么不直接去办而要通过中介?可能房主在外地,只是托中介帮忙阅读全文
posted @ 2008-10-22 22:19 lemonade 阅读(96) 评论(0) 编辑