public interface PersonProfile {
String getName();
Address getAddress();
PhoneNumber getPhoneNumber();
}
public class Employee implements PersonProfile {
protected Person parent;
public String getName() {
return this.parent.getName();
}
public Address getAddress() {
return this.parent.getAddress();
}
public PhoneNumber getPhoneNumber() {
return this.parent.getPhoneNumber();
}
}
PersonProfile是一个接口,这里之所以采用接口,是因为我们的目标在于定义一个规范,而不是进行代码重用。比起实现继承,接口继承具有更佳的灵活性。定义了这个接口之后,两者的职责就已经非常的明确了。这里使用到委托机制的这些代码都可以算是管理代码,本身并没有太大的价值,它们的管理价值是在客户端体现的。
看到了吗,在实际运用中,一定是多个技法同时使用的。可能有人问,我的设计思路和这不同。这并不是问题的关键,问题的关键在于,我们如何使用这些技法来描述问题、解决问题。在软件开发的过程中,象这种代码级别的规范是必须勤于制定的,因为这种规范往往是客户调用方和代码提供方的一种约定、一种契约。针对这些规范进行工作的衔接、文档的撰写、测试的设计都能够达成事半功倍的效果。久而久之,在软件设计中就能够形成很明显的"块状"的设计和代码,这对软件的质量是很有帮助的。
1.3 包
当早期编写程序的时候,缺少一种对代码的组织技术,因此在大学时代写项目,靠的就是目录结构。在开始设计软件之前,会先在磁盘上建立需要的目录,例如有的目录用于存放全局的函数,有的目录用于存放数据库的DDL文件,有的目录用于存放文档。短短几年后,我们已经不需要使用这么老土的技术了。在现代的语言中,有很多新的机制能够帮助我们更好的组织代码。
我们这里讨论的包主要是对源代码的组织,同时,各种平台中还存在对执行码的组织形式,例如Java平台中的Jar、.Net平台中的程序集。在C++和C#语言中,代码组织形式为命名空间(namespace),在Java语言中,组织形式为包(Package),在Eiffel语言中,组织形式为群集(cluster)。它们产生理由各不相同,命名空间主要解决的是名字冲突的问题,而包提供了一种对类和子包的打包技术,并能够和文件系统优雅的结合,此外,包还提供了额外的可见性(包内可见),群集技术就相对比较松散,它不是Eiffel语言的组织部分,仅仅是对源代码进行打包而已。
包在软件过程中是非常重要的,它是建立统一愿景的重要工具。软件开发的初期,类的设计还不明确,这时候首先确定的就应该是包图。包图不但表现了对业务领域的初步划分,还表现了开发团队以往的经验。
此外,我们知道,类和类之间最重要的就是它们相互作用,形成一个#网络。但是当这个#网络过于复杂的时候,我们就需要一些办法来组织这张网,使它更加的清晰。包的作用就是对#网络进行分块,并降低块和块之间的交互。记得设计模式中的fa?ade模式吗,这个思路是一样的。例如Java中包的成员能够看见彼此之间的保护和私有成员,这种做法就是加强了包内的类之间的耦合度,限制了包间的耦合度。
2.规范
在前一章中,我们很难为重用来定义规范,因为重用的设计需要对症下药,很难定义一个标准的设计。但是代码组织则不同,可以想出很多代码组织的规范。例如,命名规则、自文档的代码、业务实体的设计规范。代码组织的规范应该从整体开始考虑,这样的效果最为明显。
所以,最佳的规范应该是从分层设计上和包的设计上入手。软件按照什么样的原则进行分层;每一个包的含义是什么,在分层设计中起到什么样的作用;如何保存数据,如何处理数据,如何传递数据,如何表现数据。这些是非常值得,也是非常必须建立规范的。
设计上的规范能够从整体上规范代码,而代码规范能够在细节上规范代码。代码级别的规范有很多,例如sun的网站上就提供了很多的规范。
而像接口设计这样的技巧就很难定义一个规范,所以它的规范和上一章制定重用规范的思路类似。
不过,一如之前的案例,制定规范只是第一步,更重要的是在后面。
3.组织
之所以要组织代码,其中的一个原因就是为了满足组织的需要。在单人的开发环境下,代码要不要组织都是次要的,只要开发人员本人没有问题,那就没有问题。但多人的开发环境下就不同了。不同的开发人员需要对软件和软件开发有着一致的了解。在组织环境中,每个开发人员都在一定的抽象层次上为其它开发人员提供服务。开发人员之间通过这种提供服务和使用服务的方式耦合起来,如果服务效果不好,那么开发就会打折扣。
一个人写出的类或框架,别人用起来是否方便,理解起来是否容易,这些都很重要。在上面讨论接口的例子中,我们为什么一边要分离两者的职责,一边又要让角色对象具有演员对象的特征呢?既然是这样,用一个单独的类解决它们不是更好吗,何必要多此一举呢?注意到,这是两个层面的问题。一个是分工问题,一个是便利问题。回想以前我们缴费的时候,需要去电业局缴电费,去自来水公司缴水费,很不方便,后来发明了银行代扣,这样,原先要去好几个地方的,现在只需要去一个地方。角色和演员就好比电业局和自来水公司,它们的职责不同。但是这对客户端来说是不方便的,为了获取演员的姓名,我们必须这样:
// Employee的处理代码
String name = (Person)e.getPerson().getName;
为了不需要在演员和角色之间切换来切换去,我们希望客户端的代码能够简单一些:
String name = e.getName();
所以呢,这时候角色就变成了银行(当然也可以引入第三个对象来扮演银行,有的例子中确实需要这样,但这里没有必要)。客户端的代码就简化了,至于怎么获得姓名,那是你角色的事情,和客户端没有什么关系。那么,PersonProfile接口是一个什么东西呢?其实就是一种约定,是我们知道在银行可以代缴水电费这样一种知识。如果缺乏这种知识,我们就会向银行询问,你这里能代缴些什么费用啊?银行就会回答说,可以代缴水电费。这就使得接口变成了一种动态接口了。
如果我们把类本身看作是服务端,对类的调用看作是客户端。那么这里表达了一种面向对象的设计思路,就是宁可服务端复杂,也要使客户端简单。这其实也就是抽象的思路。
面向对象设计的这种特点决定了面向对象软件开发过程中的一些特色。首先,软件设计的工作量分配应当基于类进行。每一个开发人员都是为其它开发人员服务,任何一个人都需要用到其他人的代码。这种关系是分工的关系,要比非面向对象开发的分工化程度高许多。当然,这种分工是有着一定的原则的,是按照软件的分层体系来划分呢?还是按照业务需求来划分。只要能够划分清楚,任何一种方式都是可接受的。
其次,每个人保证代码质量,并维护自己的代码。在活用XP一文中,我们曾经讨论,是否采用代码集体所有制。我比较倾向于个人代码所有制,因为在这种分工的情况下,代码的质量和接口的稳定是非常重要的。所以,采用面向对象设计的团队,往往能够同时进步。正是因为客户端的要求非常的严格,迫使服务端不断的改进。
再次,对使用程度不同的类进行不同的资源分派。虽然每个类都会被其它人所使用,但有些类是很重要的,例如基础的类,或是重要的业务实体。这些类被频繁使用,保证它们的质量。
所以,代码组织的优劣对于组织来说是非常重要的,但是它还需要有过程的保证。
4.过程
执行规范需要有相应的过程保证。而规范的执行结果也同样需要有过程的保证。可能很多的软件组织都拥有自己的软件过程,但是过程是否涉及代码组织的问题?可能有,也可能没有。代码组织的上游是设计的组织,而设计组织的上游是需求的组织。上游水源的清澈与否直接影响到下游。所以在开发过程中,有两件事情是非常重要的,一是尽可能保证需求组织的正确性,而是保证代码的组织能够正确的反映需求。要做好这两点并不容易,更多的情况是,你以为自己已经做好了,其实未必。更好的做法是要求团队具备快速变化的能力,代码的组织是一个演进的过程,或者说是一个重构的过程。不断的改进,才有可能带来优秀的组织方式,不去改进,原本优秀的组织方式也会变得丑陋。