挖掘package声明的潜力 (二)
发表时间:2023-08-01 来源:明辉站整理相关软件相关文章人气:
[摘要]在哪里储存那些有用的静态程序(static utility routies)一旦你承认对两种基本的不同类型的类(通用的类和应用特定的类)的逻辑需求,你也只是离开解决另外一个棘手问题仅一步之遥:到哪里...
在哪里储存那些有用的静态程序(static utility routies)
一旦你承认对两种基本的不同类型的类(通用的类和应用特定的类)的逻辑需求,你也只是离开解决另外一个棘手问题仅一步之遥:到哪里储存那些经常在手边使用的、但却是非面向对象的、有用的静态程序。
当我看到那些在应用特定的类中嵌入完全通用的程序片的时候,我总是感到很沮丧。假设现在有一个电子商务的应用类名叫Customer,包含了如下的方法:
private String surroundedBy(String string, String quote) {
return quote + string + quote;
}
Customer类的作者在这里包含了一个工具方法来生成字符串,这个方法被标记为surroundedBy(String, String)。该方法申明为私有,大概是因为作者认为这个方法应该是实现类Customer细节中的一部分。又因为这个方法没有申明为静态的,显然地,该方法是故意被申明为一个实例方法的。看上去很不错,真的吗?那么这个方法有什么问题呢?
首先,既然该方法被申明为实例方法,那么它为什么没有依赖于Customer对象的任何的状态呢(比如:对象的域)?它没有依赖于对象中的任何域是因为它根本就不需要或者使用任何的域;这个有用的方法只需要它本身的参数来完成它的功能而不需要任何其它的东西。这是它应该作为与类独立的有用方法的一个明证;换句话说,实际上这个方法根本就不是一个实例方法。
其次,在Customer类应该包含的内容当中这个方法没有起到合理的作用,所以它不应该简单的归属于Customer类。是不是看上去有些问题了?那么下面该怎么做呢?
正确的方式是,surroundedBy()方法应该属于一个专门用来处理字符数据类型的类,而不是类似于Customer性质的应用特定的类。而不幸的是,String类本身被申明为final类型,因此不能继承它创建子类(比如说BetterString类)来安置surroundedBy()方法。一个合理的变通方式是,定义一个新类专门来处理字符数据类型,我们取名为StringUtilities(或者短一些StringUtils,或者更短一些StringKit),然后把surroundedBy()方法改为public static类型的方法,就象这样:
public class StringKit {
// .. 许多其它的处理字符数据类型的程序片断
public static String surroundedBy(String string, String quote) {
return quote + string + quote;
}
// .. 许多其它的处理字符数据类型的程序片断
} // 类StringKit结束
那么我们在执行抽取方法的重构动作后获得了什么好处呢?
从短期来看,我们获得了两个好处:
# 写了非常好的可重用的(因此也是很有价值的)代码段,可以在将来不同的项目和应用中重复使用
# 通过消除不必要的方法提升了Customer类的抽象实现(abstraction implementation)
从长期来看,上面的重构技术会带来其它的效果,甚至很可能是更加重要和有价值的效果:
# 只需写较少的新代码(想来的代码只要调用StringKit.surroundedBy()就可以了)
# 随着更多的顶层逻辑和结构变得越来越清晰,您系统的整个架构也变得越来越简单
# 软件则由于更多的代码依赖于基础的构建模块库而变得更加的强大,这些模块将会被更全面的并且比“平铺”的应用代码更加频繁的进行测试。
不幸的是,实际编写出的代码往往夹杂了太多的类似surroundedBy()这种方法,很少有Java程序员会去重用那些方法,这是因为:
# 这些方法都是申明在应用特定的代码中,这些定义也都是不通用的因此也是不可重用的
# 这些方法对于其他希望使用它们的程序员来说甚至是不可见的,因为它们被定义为私有的或者是包范围(package-scope)的方法
一个方案是系统地辨认然后移动这些放错地方的可重用方法到相关特定领域的工具类中去。请参看最后面的“静态工具方法仓库,一个个人案例”,通过一个例子来看它是如何解决问题的。
动态包层次
因为代码重构能带来积极的正面效果,你还应该坚持不懈的准备好包层次的进化(evolving)。想像一下一棵逐渐长大并且成熟的树:随着Java类以及接口数量的增长,包结构中叶子和分支的比率也在不断的增长。无论什么时候当这个比率达到某个极限,出于本能你会试图释放分支上的压力,创建子分支并且将类和接口重新分配到新的分支中去。我总是将每个包中的类和接口数保持在较低的比率下,一般在7-10的范围内。(比较一下,java.lang中30个的数量或者更多以及java.util中40个的数量或者更多,是否更多取决于具体的API的版本。有没有对java.util中这么长的可复用类列表感到过窒息(overwhelmed)呢?为每个包保持比较低的类和接口数量可以防止程序员迷失在你的API中)
随着包分支数量的增长同样也需要对子包和父包保持一定的比率。如果这个比率达到了极限,那么你也应该本能地重新整理这些子包以减低父包的压力。将包结构保持一种美学上的平衡(比如,随着时间的推移,将结构始终保持成类似不规则碎片形的树结构,始终记住软件是一门艺术,也是科学)。
到现在我听到有很大的声音在喊:“动态包层次如何适应反向兼容类库(backward-compatible library classes)中的需要呢?”很明显,这里有一个有趣的冲突:类库是需要以用户友好的形式来增长的。幸运的是,用户对类库所依赖的主要的是类库所提供出来的不变的API(比如:容易记忆的类名,精确的方法命名)。[Java中引入(import)的关键字......] (Java's import language feature makes changing the source package from which a type hails less of an obstacle than it could be otherwise.)[我尝试翻译,但总觉着翻不确切,如果谁能翻译,请一定补充一下并告诉我,谢谢!shjunsuper@263.net]
在实际情况中,每次把类或接口从一个包移动到另一个包往往为包层次的增长带来痛苦,你需要修改(bump)类库的版本号并且在你的发布申明中说明有哪些不合适的命名已经做了更改(比如:就象Sun公司在几年前处理Swing的包名一样)。以我的经验,类库的使用者更容易接受一些较小的、短期的痛苦,它们可以偶尔在一些重要的申明中对更改做个说明,申明确保公司的类库结构不会允许恶化到变成一种负担,反而成为公司的关键资产。(现今,许多的Java工具支撑这种动态的包层次,并且使得包名称和结构的更改尽可能的没有痛苦)。
包范围(package-scope)的声明
与包声明有着紧密联系的、也是很少会被Java教授者(包括书本)很好解释的、同时也可能很少被Java新手所消化吸收的另外一个Java语言特征就是:如何能正确定义类成员的(访问)范围(比如:域、方法、构建器,以及内嵌类(从Java1.1版本以后))。
没有人会对public和private访问范围有任何问题。它们之所以能被很好的理解的原因是它们意义明确,因此通常能够合适的被使用。而对于protected访问范围,则经常完全地被误解;当谈到包范围时,我们已经进入了一个真正可感知的灾难区域。
Java设计者错误的把包范围设计为缺省范围,因此它是关键字无关的包声明(所有的包都应该有关键字,没有的话就看做是缺省的包)。缺乏明显关键字是问题的根源所在:大多数的Java丛书和教程在最开始几章里的介绍中往往遗了对包范围相关的语法以及规则的介绍,这也是因为在介绍大多数的经典程序例子的时候正确的包范围问题并不是很有必要做一强调。
所以我们都被教授使用缺省的包范围声明,因为它不需要任何的关键字,因此允许我们不用任何的考虑就可以使用它了(又一个老套的懒惰标志...)。
当然,随着您Java技巧的熟练,您会逐渐认识到为类成员,尤其是域、方法以及构建器正确地设定包范围是非常重要和必须的。
域的访问范围声明:通常总是会出错
对于域来说,大约90%的的情况下都是被声明为私有类型。这是作为面向对象技术重要部分之一,封装所直接带来的结果。其次,面向对象阵营中大多数的意见是,在大多数的情况下,对象的组合比对象继承更为适合,因此受保护的访问指示符(protected)应该比私有访问指示符(private)被使用的频率低的多。你有必要把域声明为共有的(public)的唯一理由是当你要把一些常量暴露出来的时候,就象下面的情况:
public static final XXXX THE_CONSTANT;
域声明为包范围方式应该和声明非常量域为公有类型应该被视为同样的不可接受,这是因为这两种方式都破坏了对象的封装性,把他们自己的实现细节暴露出来。然而,声明为包范围的域在任何类型的Java源码中都可以看到,比如书、文章、新闻组,最后一个(决不会少见),在产品代码中。这里给出一个来自核心库中的例子(Sun的实现):在类javax.swing.ImageIcon中声明了这样一个域:
transient int loadStatus = 0;
请注意这个声明是如何包含了一个隐性的访问范围的声明,然而实际上在包javax.swing中没有任何的类来访问ImageIcon对象中的这个域。实际上,因为ImageIcon同时还声明了一个公有的对该域状态的访问控制符:
public int getImageLoadStatus(){
return loadStatus;
}
很明显的是,这个域应该被声明私有的。
方法和构造器包范围的声明
和域包范围相比较,方法和构造器的包范围定义更复杂一些。它们两者都可以被声明为以下四种访问范围:
# 公有的(Public):适用于所有的情况,都可以被访问
# 受保护的(Protected):只有子类才可以访问
# 私有的(Private):只有它自己可以访问
# 友好的(缺省):只有在同一个包下面的(不包含子目录)可以访问
明显地,如果一个子系统或者模块存在于系统自己的包下面,那么方法和构造器的包范围的声明通常是最合理的并且通常也是必须的。
Good things come in small packages
包的声明通常必须是作为第一行非注释语句出现在Java代码中的,然而,在Java语言诞生了7年之后,大多数的程序员并没有真正的领略到正确应用package关键字所带来的好处的潜力。这个小关键字可以让你通过拆分并模块化系统的架构来解决整个项目的复杂程度,并且能让你创建出长期的软件开发过程框架以实现代码的重用。这个潜力再漂亮不过了!所以,下次你创建一个新的包的时候,应该更多的更彻底的考虑一下package这个关键字的作用。这将是一项明智的、也是长期的投资。
-------------------------------------------------------------------------
静态工具类仓库,一个个人的例子
每个程序员都有他自己喜欢的工具(类)集合。我自己经常用到的工具包是我的可信赖的静态工具类集合。所有下面列出的类都只包含静态方法,所以它们永远不会被实例化。这使得它们非常的简单易用(就象java.lang.Math一样)。
类名位于包
AppKitorg.lv.lego
ArrayKitorg.lv.lego
BeansKitorg.lv.lego.beans
CheckboxKit org.lv.lego.gui
ChoiceKit org.lv.lego.gui
CLIKitorg.lv.lego
ColorKitorg.lv.lego.graphics
CombinatoricsKitorg.lv.lego.math
ConvertKitorg.lv.lego.math
DatabaseKit org.lv.lego.database
DialogKit org.lv.lego.gui
EncryptKitorg.lv.lego.math
FileKit org.lv.lego.files
GeometryKit org.lv.lego.math
GfxKitorg.lv.lego.graphics
GUIKitorg.lv.lego.gui
HTMLKit org.lv.lego.html
ImageKitorg.lv.lego.image
JavaKit org.lv.lego.java
JVMKitorg.lv.lego.java.jvm
ListKit org.lv.lego.gui
LowLevelKit org.lv.lego
MathKit org.lv.lego.math
MenuKit org.lv.lego.gui
MiscKit org.lv.lego
NetKitorg.lv.lego.comms.net
PersistencyKitorg.lv.lego
PrimeKitorg.lv.lego.math
RandomKit org.lv.lego.math
ReflectionKit org.lv.lego.java
RuntimeKitorg.lv.lego.java
StatsKitorg.lv.lego.math
StreamsKitorg.lv.lego.streams
StringKit org.lv.lego.text
SwingDialogKitorg.lv.lego.gui.swing
TableKitorg.lv.lego.gui.swing
TextComponentKitorg.lv.lego.gui
ThreadKit org.lv.lego.threads
TimeAndDateKitorg.lv.lego
UDPKitorg.lv.lego.comms.net
WebKitorg.lv.lego.comms.net
ZipKitorg.lv.lego.crunch
以上的列表显示了一些程序员个人的或者公司范围内的MiscUtils(类似的)的内容,即所谓的“让我们把所有东西都扔进去”的工具类,是一个临时的方法运输站,这些方法往往不能马上被归类。如果你知道这个日渐增长的MiscUtils类可以进行重构,那么你应该敦促这个类的作者按照以上的分类来对它进行拆分。