软件设计之Deep Module(深模块)

类是否是越小越好?最近在读John Ousterhout的《A Philosophy of Software Design》,感到做者文笔流畅,书中内容具备启发性。这里摘要一部份内容,以供开发工做中的参考、学习。html

本文连接:http://www.javashuo.com/article/p-yihkqyly-bq.html编程

转载请注明缓存

 

在软件复杂度的管理当中,最重要的技术之一是经过对系统的设计,使开发者任何在时候都只须要面对总体复杂度中的一小部分。这个过程被称为模块化设计模块化

复杂度是什么?在本文中,复杂度的定义是:和软件系统结构有关的、会致使理解和修改系统变困难的东西。函数

1,模块化设计

在模块设计中,软件系统被分解为相对独立的模块集合。模块的形式多种多样,能够是类、子系统、或服务等。在理想的世界中,每一个模块都彻底独立于其它模块:开发者在任何模块中工做的时候,都不须要知道有关其它模块的任何知识。在这种理想状态下,系统复杂度取决于系统中复杂度最高的模块。学习

固然,实践与理想不一样,系统模块间总会多少有些依赖。当一个模块变化时,其它模块可能也须要随之而改变。模块化设计的目标就是最小化模块间的依赖。spa

为了管理依赖,咱们能够把模块当作两部分:接口实现设计

接口包含了所有的在调用该模块时须要的信息。接口只描述模块作什么,但不会包含怎么作code

完成接口所作出的承诺的代码被称为实现htm

在一个特定模块内部进行工做的开发者必须知道的信息是:当前模块的接口和实现+其它被该模块使用的模块的接口。他不须要理解其它模块的实现。

在本文中,包含接口/实现的任何代码单元,都是模块。面向对象语言中的类是模块,类中的方法也是模块,非面向对象语言中的函数也是模块。高层的子系统和服务也能够被看做模块,它们的接口也许是多种形式的,好比内核调用或HTTP请求。本文中的大部份内容针对的是类,但这些技术和理论对其它类型的模块也有效。

好模块的接口远远比实现更简单。这样的模块有2个优势。首先,简单的接口最小化了模块施加给系统其他部分的复杂度。其次,若是修改模块时能够不修改它的接口,那么其余模块就不会被修改所影响。若是模块的接口远远比实现简单,那么就更有可能在不改动接口的状况对模块进行修改。

2,接口里有什么

接口中包含2种信息:正式的和非正式的。

正式的信息在代码中被显式指定,程序语言能够检查其中的部分正确性。好比,方法的签名就是正式的信息,它包含参数的名称和类型,返回值的类型,异常的信息。不少程序语言能够保证代码中对方法的调用提供了与方法定义相匹配的参数值。

接口里面也包含非正式的元素。非正式部分没法被程序语言理解或强制执行。接口的非正式部分包含一些高层行为,好比函数会根据某个参数的内容删除具备相应名字的文件。若是某个类的使用存在某种限制,好比方法的调用须要符合特定顺序,那这也属于接口的一部分。凡是开发者在使用模块时须要了解的信息,均可以算做模块接口的一部分。接口的非正式信息只能经过注释等方式描述,程序语言没法确保描述是完整而准确的。大部分接口的非正式信息都比正式信息要更多、更复杂。

清晰的接口定义有助于开发者了解在使用模块时须要知道的信息,从而避免一些问题。

3,抽象

 抽象这一术语和模块设计思想的关系很近。抽象是实体的简化视图,省略了不重要的细节。抽象颇有用,它可使咱们对细节的思考和操纵变简单。

在模块化编程中,每一个模块经过接口提供其抽象。抽象表明了函数功能的简化视图。在函数抽象的立场上,实现的细节是不重要的,因此它们被省略了。

“不重要”这个词很关键。若是没有忽略掉不重要的细节,那么抽象会变得复杂,会增长开发者的认知负担;若是忽略掉了重要的细节,那么抽象会变得错误,失去对实践的指导意义。设计抽象的关键是理解什么是重要的,并寻找最小化重要信息的设计。

依赖抽象来管理复杂度不是编程的专利,它遍及在咱们的平常生活中。就像车子会提供一个简单抽象来让咱们驾驶,并不须要咱们理解发动机、电池、ABS之类的东西。

4,深模块

最好的模块提供了强大的功能,又有着简单的接口。术语“”能够用于描述这种模块。为了让深度的概念可视化,试想每一个模块由一个长方形表示,以下图,

长方形的面积大小和模块实现的功能多少成比例。顶部边表明模块的接口,边的长度表明它的复杂度。最好的模块是深的:他们有不少功能隐藏在简单的接口后。深模块是好的抽象,由于它只把本身内部的一小部分复杂度暴露给了用户。

浅模块的接口复杂,功能却少,它没有隐藏足够的复杂度。

能够从成本与收益的角度思考模块深度。模块提供的收益是它的功能。模块的成本(从系统复杂度的角度考虑)是它的接口。接口表明了模块施加给系统其他部分的复杂度。接口越小而简单,它引入的复杂度就越少。好的模块就是那些成本低收益高的模块

某些语言中的垃圾回收(GC)是深模块的例子之一。这个模块没有接口,它在须要回收无用内存的场景下不可见地工做。在系统中加入垃圾回收的作法,缩小了系统的总接口,由于这种作法消除了用于释放对象的接口。垃圾回收的具体实现是至关复杂的,但这一复杂度在实际使用程序语言的时候是隐藏的。

5,浅模块

相对的,浅模块就是接口相对功能而言很复杂的模块。下面是个可能有些极端的例子,

private void addNullValueForAttribute(String attribute) {
  data.put(attribute, null);
}

从复杂度管理的角度来看,该方法把事情变糟了。它没有提供抽象,由于全部的功能都是在接口上可见的。思考这一接口并不会比思考它的完整实现更简单。若是方法有合适的文档,文档也不会比方法的代码具备更多信息。相比于直接操做data,它的长名字甚至会致使开发者敲击键盘的次数变多。这种方法增长了复杂度(引入了一个须要开发者了解的新接口),但并无提供与之相应的收益。注意:小的模块会更倾向于变浅。

6,Classitis

当今,深模块的价值并无被广为接受。通常常识是类须要小,而不是深。学生们被告知:类设计中最重要的事情是把大类拆分红更小的类。类似的建议还包括:“要把方法行数大于N的方法分红多个方法”,有时候N甚至只有10这么小。这会致使大量的浅模块,增长系统的总复杂度。

极端的“类应该小”的作法是一种综合症的表现,这种症状能够被称为Classitis。它源于一种错误思惟:“类是好的,因此越多类越好”。这种思想最终会致使系统层面积累了巨大的复杂度,程序风格也会变得啰嗦。

7,例子

Java类库多是Classitis的最明显例子之一。Java语言自己不须要不少小类,但Classitis文化可能已经在Java语言社区扎了根。好比,为了打开文件读取其中的序列化对象,你必须建立多种对象:

FileInputStream fileStream =
    new FileInputStream(fileName);
BufferedInputStream bufferedStream =
    new BufferedInputStream(fileStream);
ObjectInputStream objectStream =
    new ObjectInputStream(bufferedStream);

FileInputStream对象只提供初步的I/O,它不具有缓存I/O的能力,也不能读写序列化对象。BufferedInputStream和ObjectInputStream分别提供了后面两项功能。文件打开以后,fileStream和bufferedStream就没用了,将来的操做只会用到objectStream.。

必须显式单首创建BufferedInputStream对象来请求缓存,这很烦人并且易出错。若是开发者忘记建立它,就不会有缓存,并且I/O会慢。大概Java开发者会辩解说,不是全部人都须要缓存,因此它不该该包含在基本读写机制中。他们也许会说让缓存独立更好,借此用户能够选择是否使用它。提供选择空间固然很好,但接口须要设计为对经常使用场景尽量简单,几乎全部文件I/O用户都想使用缓存,因此就应该默认提供它。对于少数不须要的状况,库能够提供机制以禁用。禁用缓存的机制应该明确地在接口中分离(例如,为FileInputStream提供不一样的构造器,或者经过一个方法禁用/替换缓存机制),这样大部分开发者甚至不须要意识到它的存在。

8,结论

经过将模块的接口和实现分离,咱们能够对系统的其它部分隐藏实现的复杂度。模块的使用者只须要理解接口提供的抽象。在设计类和其它模块时,最重要的问题是让它们深,它们要对常见用例有足够简单的接口,但同时依然提供强大的功能。这就最大化地隐藏了复杂度。

相关文章
相关标签/搜索