代码大全——创建类的原因

来源:互联网 发布:java分布式锁原理 编辑:程序博客网 时间:2024/05/16 05:03

创建类的原因

如果你完全相信所读到的内容,你可能会得到这么一个概念,即认为创建类的唯一理由就是要为现实世界中的物体(object,对象)建模。而实际上,创建类的理由远远不止这一个。下面就列出一些创建类的合理原因:

为现实世界中的对象建模  为现实世界中的对象建模也许不是创建类的唯一理由,但它仍是个很好的理由!请为你程序中需要建模的每一个出现在现实世界中的对象类型创建一个类。把该对象所需的数据添加到类里面,然后编写一些服务子程序来为对象的行为建模。请参阅第6.1节中关于ADT的讨论以及其中的例子。

为抽象的对象建模  创建类的另一个合理的原因是要建立抽象对象的模型,所谓的抽象对象并不是一个现实世界中的具体对象,但它却能为另外一些具体的对象提供一种抽象。经典的Shape(形状)对象就是一个很好的例子。Circle(圆)和Square(正方形)都是真实存在的,但Shape则是对其他具体形状的一种抽象。

在程序设计中,抽象并不是像Shape一样现成就有的,因此我们必须努力工作以得出一些清晰的抽象。“从现实世界中的实体中提炼出抽象的概念”这一过程是不确定的,不同的设计者会抽象出不同的共性(generalities)来。举例来说,假如我们并不了解诸如圆、正方形和三角形这样的几何形状,就可能会得出一些更不寻常的形状,比如说南瓜的形状、大头菜的形状、或是Pontiac Aztek 似的形状。得出恰当的抽象对象是面向对象设计中的一项主要挑战。

降低复杂度  创建类的一个最重要的理由便是降低程序的复杂度。创建一个类来把信息隐藏起来,这样你就无须再去考虑它们。当然,当你写到这个类的时候还是要考虑这些信息的。但当类写好后,你就应该能够忘掉这些细节,并能在无须了解其内部工作原理的情况下使用这个类。其他那些创建类的原因——缩减代码空间、提高可维护性以及提高正确性——都是很好的,但是一旦失去了类的抽象能力,那么复杂的应用程序对于我们的智力而言将是无法管理的了。

隔离复杂度  无论复杂度表现为何种形态——复杂的算法、大型数据集、或错综复杂的通讯协议等——都容易引发错误。一旦错误发生,只要它还在类的局部而未扩散到整个程序中,找到它就会比较容易。修正错误时引起的改动不会影响到其他代码,因为只有一个类需要修改,不会碰到其他代码。如果你找到了一种更好、更简单或更可靠的算法,而原有的算法已经用类隔离起来的话,就可以很容易地把它替换掉。在开发过程中,这样做可以让你更容易地尝试更多设计方案,保留最好的一种方案。

隐藏实现细节  想把实现细节隐藏起来的这种愿望本身便是创建类的一个绝佳理由,无论实现细节是像访问数据库那般复杂,还是像决定用数值还是字符串来存储某个特定数据成员那般寻常。

限制变动的影响范围  把容易变动的部分隔离开来,这样就能把变动所带来的影响限制在一个或少数几个类的范围内。把最容易变动的部分设计成最容易修改的。容易变动的部分有硬件依赖性、输入/输出、复杂数据类型、业务逻辑等等。在第5.3节“隐藏秘密(信息隐藏)”一节中介绍了几种常见的引起变化的根源。

隐藏全局数据  如果你需要用到全局数据,就可以把它的实现细节藏到某个类的接口背后。与直接使用全局数据相比,通过访问器子程序(access routine)来操控全局数据有很多好处。你可以改变数据结构而无须修改程序本身。你可以监视对这些数据的访问。“使用访问器子程序”的这条纪律还会促使你去思考有关数据是否就应该是全局的;经常你会豁然开朗地发现,“全局数据”原来只是对象的数据而已。

让参数传递更顺畅  如果你需要把一个参数在多个子程序之间传递,这有可能表明应该把这些子程序重构到一个类里,把这个参数当做对象数据来共享。实质上,让参数传递得更顺畅并不是目标,但把大量的数据到处传递是在暗示换一种类的组织方式可能会更好。

建立中心控制点  在一个地方来控制一项任务是个好主意。控制可以表现为很多形式。了解一张表中记录的数目是一种形式;对文件、数据库连接、打印机等设备进行的控制又是一种。用一个类来读写数据库则是集中控制的又一种形式。如果需要把数据库转换为平坦的文件或者内存数据,有关改动也只会影响一个类。

集中控制这一概念和信息隐藏有些相似,但它具有独特的启发式功用,值得把它放到你的编程工具箱中。

让代码更易于重用  将代码放入精心分解(well-factored)的一组类中,比起把代码全部塞进某个更大的类里面,前者更容易在其他程序中重用。如果有一部分代码,它们只是在程序里的一个地方调用,只要它可以被理解为一个较大类的一部分,而且这部分代码可能会在其他程序中用到,就可以把它提出来形成一个单独的类。

美国NASA的软件工程实验室(Software Engineering Laboratory)曾经研究了10个积极追求代码重用的项目(McGarry, Waligora, and McDermott 1989)。研究结果表明,无论采用面向对象的设计方法还是以功能为导向的(functionally oriented)设计方法,在最初的项目中都没能太多地重用之前项目中的代码,因为之前的项目尚未形成充分的代码基础(code base)。然而到了后来,以功能为导向进行设计(functional design)的项目能重用之前项目中约35%的代码。而使用面向对象方法的项目则能重用之前项目中超过70%的代码。如果提前规划一下就能让你少写70%的代码,那当然要这样做了!

值得注意的是,NASA这种创建可重用的类的方法并未涉及“为重用而设计”。NASA在其项目结束时挑出了可供重用的备选代码。然后,他们进行了必要的工作来让这些代码可以重用,这些工作或被当做是主项目后期的一个特殊项目,或被当做是新项目的第一个步骤。这种方法有助于避免“镀金”——增加一些并不实际需要的、但却会增加不必要的复杂度的功能。

为程序族做计划  如果你预计到某个程序会被修改,你可以把预计要被改动的部分放到单独的类里,同其他部分隔离开,这是个好主意。之后你就可以只修改这个类或用新的类来取代它,而不会影响到程序的其余部分了。仔细考虑整个程序族(family of programs)的可能情况,而不单是考虑单一程序的可能情况,这又是一种用于预先应对各种变化的强有力的启发式方法(Parnas 1976)。

几年前我管理过一个团队,我们为客户开发一系列用于保险销售的程序。我们必须按照客户特定的保险费率、报价表格式等来定制每个程序。然而这些程序的很多部分都是相同的:用来输入潜在客户的信息的类、用来把信息存到客户数据库的类、用来查询费率的类、计算一个组的全部费率的类,等等。开发团队对程序的结构进行了规划,把每个能根据客户要求进行变化的部分都放到单独的类里面。按照开始的编程任务来计算的话,我们可能要花大约三个月的时间,但当有了新客户之后,我们仅仅需要为该客户开发出一些新类,然后让这些新类同其余代码一起工作。定制一套软件只用几天的工夫!

把相关操作包装到一起  即便你无法隐藏信息、共享数据或规划灵活性,你仍然可以把相关的操作合理地分组,比如分为三角函数、统计函数、字符串处理子程序、位操作子程序以及图形子程序等等。类是把相关操作组合在一起的一种方法。除此之外,根据你所使用的编程语言不同,你还可以使用包(package)、命名空间(namespace)或头文件等方法。

实现某种特定的重构  第24章“重构”中所描述的很多特定的重构方法都会生成新的类,包括把一个类转换为两个、隐藏委托、去掉中间人以及引入扩展类等。为了能更好地实现本节所描述的任何一个目标,这些都是产生各种新类的动机。

Classes to Avoid

应该避免的类

尽管通常情况下类是有用的,但你也可能会遇到一些麻烦。下面就是一些应该避免创建的类:

避免创建万能类(god class)  要避免创建什么都知道、什么都能干的万能类。如果一个类把工夫都花在用Get()方法和Set()方法向其他类索要数据(也就是说,深入到其他类的工作中并告诉它们该如何去做)的话,请考虑是否应该把这些功能组织到其他那些类中去,而不要放到万能类里(Riel 1996)。

消除无关紧要的类  如果一个类只包含数据但不包含行为的话,应该问问自己,它真的是一个类吗?同时应该考虑把这个类降级,让它的数据成员成为一个或多个其他类的属性。

避免用动词命名的类  只有行为而没有数据的类往往不是一个真正的类。请考虑把类似DatabaseInitialization(数据库初始化)或StringBuilder(字符串构造器)这样的类变成其他类的一个子程序。

总结:创建类的理由

下面总结一下创建类的合理原因:

■           对现实世界中的对象建模

■           对抽象对象建模

■           降低复杂度

■           隔离复杂度

■           隐藏实现细节

■           限制变化所影响的范围

■           隐藏全局数据

■           让参数传递更顺畅

■           创建中心控制点

■           让代码更易于重用

■           为程序族做计划

■           把相关操作放到一起

■           实现特定的重构

Language-Specific Issues

与具体编程语言相关的问题

不同编程语言在实现类的方法上有着很有意思的差别。请考虑一下如何在一个派生类中通过覆盖成员函数来实现多态。在Java中,所有的方法默认都是可覆盖的,方法必须被定义成final才能阻止派生类对它进行覆盖。在C++中,默认是不可以覆盖方法的,基类中的方法必须被定义成virtual才能被覆盖。而在Visual Basic中,基类中的子程序必须被定义为overridable,而派生类中的子程序也必须要用overrides关键字。

下面列出跟类相关的,不同语言之间有着显著差异的一些地方:

■           在继承层次中被覆盖的构造函数和析构函数的行为

■           在异常处理时构造函数和析构函数的行为

■           默认构造函数(即无参数的构造函数)的重要性

■           析构函数或终结器(finalizer)的调用时机

■           和覆盖语言内置的运算符(包括赋值和等号)相关的知识

■           当对象被创建和销毁时,或当其被声明时,或者它所在的作用域退出时,处理内存的方式

关于这些事项的详细论述超出了本书的范围,不过在“更多资源”一节中提供了一些与特定语言相关的很好资源。


原创粉丝点击