
正文
java代码重构最佳实践,代码重构方法和经验
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
如何写出更好的Java代码
1. 优雅需要付出代价。
从短期利益来看,对某个问题提出优雅的解决方法,似乎可能花你更多的时间。但当它终于能够正确执行并可轻易套用于新案例中,不需要花上数以时计,甚至以天计或以月计的辛苦代价时,你会看得到先前所花功夫的回报(即使没有人可以衡量这一点)。这不仅给你一个可更容易开发和调试的程序,也更易于理解和维护。这正是它在金钱上的价值所在。这一点有赖某种人生经验才能够了解,因为当你努力让某一段程序代码变得比较优雅时,你并不是处于一种具生产力的状态下。但是,请抗拒那些催促你赶工的人们,因为那么做只会减缓你的速度罢了。
2. 先求能动,再求快。
即使你已确定某段程序代码极为重要,而且是系统的重要瓶颈,这个准则依然成立。尽可能简化设计,让系统能够先正确动作。如果程序的执行不够快,再量测其效能。几乎你总是会发现,你所认为的”瓶颈”其实都不是问题所在。把你的时间花在刀口上吧。
3. 记住”各个击破”的原理。
如果你所探讨的问题过于混杂,试着想像该问题的基本动作会是什么,并假设这一小块东西能够神奇地处理掉最难的部分。这”一小块”东西其实就是对象–请撰写运用该对象的程序代码,然后检视对象,并将其中困难的部分再包装成其他对象,依此类推。
4. 区分class开发者和class使用者(使用端程序员)。
Class 使用者扮演着”客户”角色,不需要(也不知道)class的底层运作方式。Class开发者必须是class设计专家,并撰写class,使它能够尽可能被大多数新手程序员所用,而且在程序中能够稳当执行。一套程序库只有在具备通透性的情况下,使用起来才会容易。
5.当你撰写class时,试着给予明了易懂的名称,减少不必要的注解。
你给客户端程序员的接口,应该保持概念上的单纯性。不了这个目的,当函数的重载(overloading)适合制作出直觉、易用的接口时,请善加使用。
6. 也必你的分析和设计必须让系统中的classes保持最少,须让其Public interfaces保持最少,以及让这些classes和其他classes之间的关联性( 尤其是base classes)保持最少。
如果你的设计所得结果更甚于此,请问问自己,是否其中每一样东西在整个程序生命期中都饶富价值?如果并非如此,那么,维护它们会使你付出代价。开发团队的成员都有不维护”无益于生产力提升”的任何东西的倾向;这是许多设计方法无法解释的现象。
7. 让所有东西尽量自动化。先撰写测试用的程序代码(在你撰写class之前),并让它和class结合在一起。请使用makefile或类似工具,自动进行测试动作。
通过这种方式,只要执行测试程序,所有的程序变动就可以自动获得验证,而且可以立即发现错误。由于你知道的测试架构所具备的安全性,所以当你发现新的需求时,你会更勇于进行全面修改。请记住,程序语言最大的改进,是来自型别检查、异常处理等机制所赋予的内置测试动作。但这些功能只能协助你到达某种程度。开发一个稳固系统时,你得自己验证自己的classes或程序的性质。
8. 在你撰写class之前先写测试码,以便验证你的class 是否设计完备。如果你无法撰写测试码,你便无法知道你的class 的可能长相。撰写测试码通常能够显现出额外的特性(features)或限制 ( constraints)__它们并不一定总是能够在分析和设计过程中出现。测试码也可做为展示class 用法的示例程序。
9. 所有软件设计上的问题,都可以通过”引入额外的概念性间接层(conceptual indirection)”加以简化。这个软件工程上的基础法则是抽象化概念的根据,而抽象化概念正是面向对象程序设计的主要性质。
10. 间接层(indirection)应该要有意义(和准则-9致)。
这里所指的意义可以像”将共用程序代码置于惟一函数”这么简单。如果你加入的间接层(或抽象化、或封装等等)不具意义,它可能就和没有适当的间接层一样糟糕。
11. 让class尽可能微小而无法切割(atomic)。
赋予每个class单一而清楚的用途。如果你的classes或你的系统成长得过于复杂,请将复杂的classes切割成比较简单的几个classes。最明显的一个判断指针就是class的大小:如果它很大,那么它工作量过多的机会就可能很高,那就应该被切割。重新设计class的建议线索是:
1) 复杂的switch语句:请考虑运用多态(Polymorphism)。
2) 许多函数各自处理类型极为不同的动作:请考虑切割为多个不同的(classes)。
12. 小心冗长的引数列(argument lists)。
冗长的引数列会使函数的调用动作不易撰写、阅读、维护。你应该试着将函数搬移到更适当的class中,并尽量以对象为引数。
13. 不要一再重复。
如果某段程序代码不断出现于许多derived class函数中,请将该段程序代码置于某个base class 函数内,然后在derived class函数中调用。这么做不仅可以省下程序代码空间,也可以让修改该段程序代码动作更易于进行。有时候找出此种共通程序代码还可以为接口增加实用功能。
14. 小心switch语句或成串的if-else 子句。
通常这种情况代表所谓的”type-check coding”。也就是说究竟会执行哪一段程序代码,乃是依据某种型别信息来做抉择(最初,确切型别可能不十分明显)。你通常可以使用继承和多态来取代此类程序代码;Polymorphical method (多态函数)的调用会自动执行此类型别检验,并提供更可靠更容易的扩充性。
15. 从设计观点来看,请找出变动的事物,并使它和不变的事物分离。
也就是说,找出系统中可能被你改变的元素,将它们封装于classes中。你可以在《Thinking in Patterns with Java》(可免费下载于 www. BruceEckel. Com)大量学习到这种观念。
16. 不要利用subclassing来扩充基础功能。
如果某个接口元素对class而言极重要,它应该被放在base class 里头,而不是直到衍生(derivation)时才被加入。如果你在继承过程中加入了函数,或许你应该重新思考整个设计。
17. 少就是多。
从class 的最小接口开始妨展,尽可能在解决问题的前提下让它保持既小又单纯。不要预先考量你的class被使用的所有可能方式。一旦class被实际运用,你自然会知道你得如何扩充接口。不过,一旦class被使用后,你就无法在不影响客户程序代码的情况下缩减其接口。如果你要加入更多函数倒是没有问题–不会影响既有的客户程序代码,它们只需重新编译即可。但即使新函数取代了旧函数的功能,也请你保留既有接口。如果你得通过”加入更多引数”的方式来扩充既有函数的接口,请你以新引数写出一个重载化的函数;通过 这种方式就不会影响既有函数的任何客户了。
18. 大声念出你的classes,确认它们符合逻辑。
请base class和derived class 之间的关系是”is-a”(是一种),让class和成员对象之间的关系是”has-a”(有一个)。
19. 当你犹豫不决于继承(inheritance)或合成(组合,composition)时,请你问问自己,是否需要向上转型(upcast)为基础型别。
如果不需要,请优先选择合成(也就是是使用成员对象)。这种作法可以消除”过多基础型别”。如果你采用继承,使用者会认为他们应该可以向上转型。
20. 运用数据成员来表示数值的变化,运用经过覆写的函数(overrided method)来代表行为的变化 。
也就是说,如果你找到了某个 class, 带有一些状态变量,而其函数会依据这些变量值切换不同的行为,那么你或许就应该重新设计,在subclasses 和覆写后的函数(overrided methods)中展现行为止的差异。
21. 小心重载(overloading)。
函数不应该依据引数值条件式地选择执行某一段程序代码。这种情况下你应该撰写两个或更多个重载函数(overloaded methods)
22. 使用异常体系(exception hierarchies)
最好是从Java标准异常体系中衍生特定的classes, 那么,捕捉异常的人便可以捕捉特定异常,之后才捕捉基本异常。如果你加入新的衍生异常,原有的客户端程序仍能通过其基础型别来捕捉它。
23. 有时候简单的聚合(aggregation)就够了。
飞机上的”旅客舒适系统”包括数个分离的元素:座椅、空调、视讯设备等等,你会需要在飞机上产生许多这样的东西。你会将它们声明为Private成员并开发出一个全新的接口吗?不会的,在这个例子中,元素也是Public接口的一部分,所以仍然是安全的。当然啦,简单聚合并不是一个常被运用的解法,但有时候的确是。
24. 试着从客户程序员和程序维护的角度思考。
你的class应该设计得尽可能容易使用。你应该预先考量可能性有的变动,并针对这些 可能的变动进行设计,使这些变动日后可轻易完成。
25. 小心”巨大对象并发症”。
这往往是刚踏OOP领域的过程式(procedural)程序员的一个苦恼,因为他们往往最终还是写出一个过程式程序,并将它们摆放到一个或两个巨大对象中。注意,除了application framework (应用程序框架,译注:一种很特殊的、大型OO程序库,帮你架构程序本体)之外,对象代表的是程序中的观念,而不是程序本身。
26. 如果你得用某种丑陋的方式来达成某个动作,请将丑陋的部分局限在某个class里头。
27. 如果你得用某种不可移植方式来达成某个动作,请将它抽象化并局限于某个class里头。这样一个”额外间接层”能够防止不可移植的部分扩散到整个程序。这种作法的具体呈现便是Bridge设计模式(design pattern)。
28. 对象不应仅仅只用来持有数据。
对象也应该具有定义明确界限清楚的行为。有时候使用”数据对象”是适当的,但只有在通用形容器不适用时,才适合刻意以数据对象来包装、传输一群数据项。
29. 欲从既有的classes身上产生新的classes时,请以组合(composition)为优先考量。
你应该只在必要时才使用继承。如果在组合适用之处你却选择了继承,你的设计就渗杂了非必要的复杂性。
30. 运用继承和函数覆写机制来展现行为上的差异,运用fields(数据成员)来展现状态上的差异。
这句话的极端例子,就是继承出不同的classes表现各种不同的颜色,而不使用”color”field.
31. 当心变异性(variance)。
语意相异的两个对象拥有相同的动作(或说责任)是可能的。OO世界中存在着一种天生的引诱,让人想要从某个class继承出另一个subclass,为的是获得继承带来的福利。这便是所谓”变异性”。但是,没有任何正当理由足以让我们强迫制造出某个其实并不存在的superclass/subclass关系。比较好的解决方式是写出一个共用的base class,它为两个derived classes制作出共用接口–这种方式会耗用更多空间,但你可以如你所盼望地从继承机制获得好处,而且或许能够在设计上获得重大发现。
32. 注意继承上的限制。
最清晰易懂的设计是将功能加到继承得来的class里头;继承过程中拿掉旧功能(而非增加新功能)则是一种可疑的设计。不过,规则可以打破。如果你所处理的是旧有的class程序库,那么在某个class的subclass限制功能,可能会比重新制定整个结构(俾使新class得以良好地相称于旧 class)有效率得多。
33. 使用设计模式(design patterns)来减少”赤裸裸无加掩饰的机能(naked functionality)”。
举个例子,如果你的class只应该产出惟一一个对象,那么请不要以加思索毫无设计的手法来完成它,然后撰写”只该产生一份对象”这样的注解就拍拍屁股走人。请将它包装成singleton(译注:一个有名的设计模式,可译为”单件”)。如果主程序中有多而混乱的”用以产生对象”的程序代码,请找出类似 factory method这样的生成模式(creational patterns),使价钱可用以封装生成动作减少”赤裸裸无加掩饰的机能”(naked functionality)不仅可以让你的程序更易理解和维护,也可以阻止出于好意却带来意外的维护者。
34. 当心”因分析而导致的瘫痪(analysis paralysis)”。
请记住,你往往必须在获得所有信息之前让项目继续前进。而且理解未知部分的最好也最快的方式,通常就是实际前进一步而不只是纸上谈兵。除非找到解决办法,否则无法知道解决办法。Java拥有内置的防火墙,请让它们发挥作用。你在单一class或一组classes中所犯的错误,并不会伤害整个系统的完整性。
35. 当你认为你已经获得一份优秀的分析、设计或实现时,请试着加以演练。
将团队以外的某些人带进来-他不必非得是个顾问不可,他可以是公司其他团队的成员。请那个人以新鲜的姿态审视你们的成果,这样可以在尚可轻易修改的阶段找出问题,其收获会比因演练而付出的时间和金钱代价来得高。实现 (Implementation)
36. 一般来说,请遵守Sun的程序编写习惯。
价钱可以在以下网址找到相关文档:java.sun.com/docs/codeconv/idex.html。本书尽可能遵守这些习惯。众多Java程序员看到的程序代码,都有是由这些习惯构成的。如果你固执地停留在过去的编写风格中,你的(程序代码)读者会比较辛苦。不论你决定采用什么编写习惯,请在整个程序中保持一致。你可以在home.wtal.de/software-solutions/jindent上找到一个用来重排Java程序的免费工具。
37. 无论使用何种编写风格,如果你的团队(或整个公司,那就更好了)能够加以标准化,那么的确会带来显著效果。这代表每个人都可以在其他人不遵守编写风格修改其作品,这是个公平的游戏。标准化的价值在于,分析程序代码时所花的脑力较小,因而可以专心于程序代码的实质意义。
38. 遵守标准的大小写规范。
将 class名称的第一个字母应为大写。数据成员、函数、对象(references)的第一个字母应为小写。所有识别名称的每个字都应该连在一块儿,所有非首字的第一个字母都应该大写。例如: ThisIsAClassName thisIsAMethodOrFieldName 如果你在static final 基本型别的定义处指定了常量初始式(constant initializers),那么该识别名称应该全为大写,代表一个编译期常量。 Packages是个特例,其名称皆为小写,即使非首字的字母亦是如此。域名(org, net, edu 等等)皆应为小写。(这是Java 1.1迁移至Java 2时的一项改变) 。
39、不要自己发明”装饰用的”Private数据成员名称。
通常这种的形式是在最前端加上底线和其他字符,匈牙利命名法(Hungarian notation)是其中最差的示范。在这种命名法中,你得加入额外字符来表示数据的型别、用途、位置等等。仿佛你用的是汇编语言(assembly language)而编译器没有提供任何协肋似的。这样的命名方式容易让人混淆又难以阅读,也不易推行和维护。就让classes和packages来进行”名称上的范
围制定(name scoping)”吧。
40、当你拟定通用性的class时,请遵守正规形式(canonical form)。
包括equals( )、hashCode( )、clone( ) ( 实现出Cloneable),并实现出Comparable和Serialiable等等。
相关问答
Q1: Java代码优化的常用方法有什么
1)尽量指定类、方法的final修饰符。带有final修饰符的类是不可派生的,Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,此举能够使性能平均提高50%。
2)尽量重用对象。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此生成过多的对象将会给程序的性能带来很大的影响。
3)尽可能使用局部变量。调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建速度较慢。
4)慎用异常。异常对性能不利,只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
5)乘法和除法使用移位操作。用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的,但是移位操作虽然快,可能会使代码不太好理解,因此最好加上相应的注释。
6)尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用 Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销。
尽量在合适的场合使用单例。使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例。
Q2: 在JAVA中节点同构不同构是什么意思
你说的应该是重构的意思。
java重构:指程序员对已有程序在尽量不改变接口的前提下,进行重新编写代码的工作,一般有以下几方面:1、去除已知bug。2、提高程序运行效率。3、增加新的功能。
Java代码的重构模式主要有三种:
重命名方法重构模式、引入解释性变量重构模式、以查询取代临时变量重构模式
重命名方法重构模式建议执行如下的步骤来完成:
1.建立一个具有新名称的方法
2.将旧方法的方法体复制进新方法
3.讲旧方法的方法体修改为调用新方法
4.将所有引用旧方法的地方修改为引用新方法
5.删除旧方法
引入解释性变量重构模式步骤相对简单,如下:
1.声明一个局部变量,并将其初始化为需要替换的表达式部分
2.对于复杂的表达式,用新的局部变量代替其中需要替换的部分
3.对于该表达式的其他部分,重复这个过程
以查询取代临时变量重构模式的步骤如下:
1.找出只赋值了一次的局部变量
2.将该局部变量声明为final并重新编译(以确保该变量的确只赋值了一次)
3.复制赋值语句的等号右侧的表达式,并将其修改为一个新方法的方法体
4.将赋值语句的等号右侧更改为对新方法的调用
5.将所有对该局部变量的使用替换为对新方法的调用
6.删除赋值语句以及局部变量的声明
重构举例:(简化代码、提升效率)
重构前:
if(list != null list.size() 0){
for(int i = 0; i list.size(); i++){
//skip...
}
}
重构后
if(list != null){
for(int i = 0, len = list.size(); i len; i++){
//skip...
}
}
Q3: Java升级篇:ANT十五大最佳实践详解
在Ant出现之前 构建和部署Java应用需要使用包括特定平台的脚本 Make文件 各种版本的IDE甚至手工操作的 大杂烩 现在 几乎所有的开源Java项目都在使用Ant 大多数公司的内部项目也在使用Ant Ant在这些项目中的广泛使用自然导致了读者对一整套Ant最佳实践的迫切需求 本文总结了我喜爱的Ant技巧或最佳实践 多数是从我亲身经历的项目错误或我听说的其他人经历的 恐怖 故事中得到灵感的 比如 有人告诉我有个项目把XDoclet 生成的代码放入带有锁定文件功能的版本控制工具中 当开发者修改源代码时 他必须记住手工检出(Check out)并锁定所有将要重新生成的文件 然后 手工运行代码生成器 只到这时他才能够让Ant编译代码 这一方法还存在如下一些问题 生成的代码无法存储在版本控制系统中 Ant(本案例中是Xdoclet)应该自动确定下一次构建涉及的源文件 而不应由程序员手工确定 Ant的构建文件应该定义好正确的任务依赖关系 这样程序员就不必为了完成构建而不得不按照特定顺序调用任务 当我开始一个新项目时 我首先编写Ant构建文件 Ant文件明确地定义构建的过程 并被团队中的每个程序员使用 本文所列的技巧基于这样的假定 Ant构建文件是一个必须仔细编写的重要文件 它应在版本控制系统中得到维护 并被定期进行重构 下面是我的十五大Ant最佳实践 采用一致的编码规范 Ant用户有的喜欢有的痛恨其构建文件的XML语法 与其跳进这一令人迷惑的争论中 不如让我们先看一些能保持XML构建文件简洁的方法 首先也是最重要的 花费时间格式化你的XML让它看上去很清晰 不论XML是否美观 Ant都可以工作 但是丑陋的XML很难令人读懂 倘若你在任务之间留出空行 有规则的缩进 每行文字不超过 列左右 那么XML令人惊讶地易读 再加上使用能够高亮XML语法的优秀编辑器或IDE工具 你就不会有阅读的麻烦 同样 精选含意明确 容易读懂的词汇来命名任务和属性 比如 dir reports就比rpts好 特定的编码规范并不重要 只要拿出一套规范并坚持使用就行 将build xml放在项目根目录中 Ant构建文件build xml可以放在任何位置 但是放在项目顶级目录中可以保持项目简洁 这是最常用的规范 开发者能够在顶级目录中找到预期的build xml 把构建文件放在根目录中 也能够使人容易了解项目目录树中不同目录之间的逻辑关系 以下是一个典型的项目目录层次 [root dir]| build xml+ src+ lib (包含第三方 JAR包)+ build (由 build任务生成)+ dist (由 build任务生成)当build xml在顶级目录时 假设你处于项目某个子目录中 只要输入 ant find pile 命令 不需要改变工作目录就能够以命令行方式编译代码 参数 find告诉Ant寻找存在于上级目录中的build xml并执行 使用单一的构建文件 有人喜欢将一个大项目分解成几个小的构建文件 每个构建文件分担整个构建过程的一小部分工作 这确实是看法不同的问题 但是应该认识到 将构建文件分割会增加对整体构建过程的理解难度 要注意在单一构建文件能够清楚表现构建层次的情况下不要过工程化(over engineer) 即使你把项目划分为多个构建文件 也应使程序员能够在项目根目录下找到核心build xml 尽管该文件只是将实际构建工作委派给下级构建文件 也应保证该文件可用 提供良好的帮助说明 应尽量使构建文件自文档化 增加任务描述是最简单的方法 当你输入ant projecthelp时 你就可以看到带有描述的任务清单 比如 你可以这样定义任务 target name= pile description= Compiles code output goes to the build dir 最简单的规则是把所有你想让程序员通过命令行就可以调用的任务都加上描述 对于一般用来执行中间处理过程的内部任务 比如生成代码或建立输出目录等 就无法使用描述属性 这时 可以通过在构建文件中加入XML注释来处理 或者专门定义一个help任务 当程序员输入ant help时来显示详细的使用说明 target name= help description= Display detailed usage information echoDetailed help /echo/target 提供清除任务 每个构建文件都应包含一个清除任务 用来删除所有生成的文件和目录 使系统回到构建文件执行前的初始状态 执行清空任务后还存在的文件都应处在版本控制系统的管理之下 比如 target name= clean description= Destroys allgenerated files and dirs delete dir= ${dir build} /delete dir= ${dir dist} //target除非是在产生整个系统版本的特殊任务中 否则不要自动调用clean任务 当程序员仅仅执行编译任务或其他任务时 他们不需要构建文件事先执行既令人讨厌又没有必要的清空任务 要相信程序员能够确定何时需要清空所有文件 使用ANT管理任务从属关系 假设你的应用由Swing GUI组件 Web界面 EJB层和公共应用代码组成 在大型系统中 你需要清晰地定义每个Java包属于系统的哪一层 否则任何一点修改都要被迫重新编译成百上千个文件 糟糕的任务从属关系管理会导致过度复杂而脆弱的系统 改变GUI面板的设计不应造成Servlet和EJB的重编译 当系统变得庞大后 稍不注意就可能将依赖于客户端的代码引入到服务端 这是因为典型的IDE项目文件编译任何文件都使用单一的classpath 而Ant能让你更有效地控制构建活动 设计你的Ant构建文件编译大型项目的步骤 首先 编译公共应用代码 将编译结果打成JAR包文件 然后 编译上一层的项目代码 编译时依靠第一步产生的JAR文件 不断重复这一过程 直到最高层的代码编译完成 分步构建强化了任务从属关系管理 如果你工作在底层Java框架上 偶然引用到高层的GUI模板组件 这时代码不需要编译 这是由于构建文件在编译底层框架时在源路径中没有包含高层GUI面板组件的代码 定义并重用文件路径 如果文件路径在一个地方一次性集中定义 并在整个构建文件中得到重用 那么构建文件更易于理解 以下是这样做的一个例子 project name= sample default= pile basedir= path id= mon pathelementlocation= ${jdom jar withpath} / etc /pathpath id= classpath client pathelement location= ${guistuff jar withpath} /pathelement location= ${another jar withpath} /! reuse the mon classpath path refid= mon //pathtarget name= mon depends= prepare javac destdir= ${dir build} srcdir= ${dir src} classpathrefid= mon /includename= /oreilly/mon/** //javac/target/project当项目不断增长构建日益复杂时 这一技术越发体现出其价值 你可能需要为编译不同层次的应用定义各自的文件路径 比如运行单元测试的 运行应用程序的 运行Xdoclet的 生成JavaDocs的等等不同路径 这种组件化路径定义的方法比为每个任务单独定义路径要优越得多 否则 很容易丢失任务从属关系的轨迹 定义恰当的任务从属关系 假设dist任务从属于jar任务 那么哪个任务从属于pile任务哪个任务从属于prepare任务呢?Ant构建文件最终定义了任务的从属关系图 它必须被仔细地定义和维护 应该定期检查任务的从属关系以保证构建工作得到正确执行 大的构建文件随着时间推移趋向于增加更多的任务 所以到最后可能由于不必要的从属关系导致构建工作非常困难 比如 你可能发现在程序员只需编译一些没有使用EJB的GUI代码时又重新生成了EJB代码 以 优化 的名义忽略任务的从属关系是另一种常见的错误 这种错误迫使程序员为了得到恰当的结果必须记住并按照特定的顺序调用一串任务 更好的做法是 提供描述清晰的公共任务 这些任务包含正确的任务从属关系 另外提供一套 专家 任务让你能够手工执行个别的构建步骤 这些任务不提供完整的构建过程 但是让那些专家用户在快速而恼人的编码期间能够跳过某些步骤 使用属性 任何需要配置或可能发生变化的信息都应作为Ant属性定义下来 对于在构建文件中多次出现的值也同样处理 属性既可以在构建文件头部定义 也可以为了更好的灵活性而在单独的属性文件中定义 以下是在构建文件中定义属性的样式 project name= sample default= pile basedir= property name= dir build value= build /property name= dir src value= src /property name= jdom home value= /java tools/jdom b /property name= jdom jar value= jdom jar / lishixinzhi/Article/program/Java/ky/201311/28340
Q4: 常见代码重构技巧(非常实用)
1_代码重构漫画.jpeg
项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
造成这样的原因往往有以下几点:
对于此类问题,业界已有有很好的解决思路:通过持续不断的重构将代码中的“坏味道”清除掉。
重构一书的作者Martin Fowler对重构的定义:
根据重构的规模可以大致分为大型重构和小型重构:
大型重构 :对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。
小型重构 :对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。什么时候重构 新功能开发、修bug或者代码review中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。
2_代码常见问题.png
代码重复
方法过长
过大的类
逻辑分散
严重的情结依恋
数据泥团/基本类型偏执
不合理的继承体系
过多的条件判断
过长的参数列
临时变量过多
令人迷惑的暂时字段
纯数据类
不恰当的命名
过多的注释
3_代码质量如何衡量.jpg
代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。
4_SOLID原则.png
一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类可以扩展父类的功能,但不能改变父类原有的功能
调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
一个对象应该对其他对象保持最少的了解
尽量使用合成/聚合的方式,而不是使用继承。
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
image.png
模块结构说明
代码开发要遵守各层的规范,并注意层级之间的依赖关系。
多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。
方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。
意图导向编程 :把处理某件事的流程和具体做事的实现方式分开。
将函数放进一个单独对象中,如此一来局部变量就变成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
方法参数比较多时,将参数封装为参数对象
任何有返回值的方法,都不应该有副作用
临时变量仅使用一次或者取值逻辑成本很低的情况下
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途
把复杂的条件表达式拆分成多个条件表达式,减少嵌套。嵌套了好几层的if - then-else语句,转换为多个if语句
当出现大量类型检查和判断时,if else(或switch)语句的体积会比较臃肿,这无疑降低了代码的可读性。 另外,if else(或switch)本身就是一个“变化点”,当需要扩展新的类型时,我们不得不追加if else(或switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。
非正常业务状态的处理,使用抛出异常的方式代替返回错误码
某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。
当使用一个方法返回的对象时,而这个对象可能为空,这个时候需要对这个对象进行操作前,需要进行判空,否则就会报空指针。当这种判断频繁的出现在各处代码之中,就会影响代码的美观程度和可读性,甚至增加Bug的几率。
空引用的问题在Java中无法避免,但可以通过代码编程技巧(引入空对象)来改善这一问题。
根据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。
此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。
继承使实现代码重用的有力手段,但这并非总是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱。与方法调用不同的是,继承打破了封装性。子类依赖于其父类中特定功能的实现细节,如果父类的实现随着发行版本的不同而变化,子类可能会遭到破坏,即使他的代码完全没有改变。
举例说明,假设有一个程序使用HashSet,为了调优该程序的性能,需要统计HashSet自从它创建以来添加了多少个元素。为了提供该功能,我们编写一个HashSet的变体。
通过在新的类中增加一个私有域,它引用现有类的一个实例,这种设计被称为组合,因为现有的类变成了新类的一个组件。这样得到的类将会非常稳固,它不依赖现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。许多设计模式使用就是这种套路,比如代理模式、装饰者模式
继承与组合如何取舍
Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从Java8为接口增加缺省方法(default method),这两种机制都允许为实例方法提供实现。主要区别在于,为了实现由抽象类定义的类型,类必须称为抽象类的一个子类。因为Java只允许单继承,所以用抽象类作为类型定义受到了限制。
接口相比于抽象类的优势:
接口虽然提供了缺省方法,但接口仍有有以下局限性:
接口缺省方法的设计目的和优势在于:
为了接口的演化
可以减少第三方工具类的创建
可以避免创建基类
由于接口的局限性和设计目的的不同,接口并不能完全替换抽象类。但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。 接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法(Template Method)设计模式。
Image [5].png
接口Protocol:定义了RPC协议层两个主要的方法,export暴露服务和refer引用服务
抽象类AbstractProtocol:封装了暴露服务之后的Exporter和引用服务之后的Invoker实例,并实现了服务销毁的逻辑
具体实现类XxxProtocol:实现export暴露服务和refer引用服务具体逻辑
由于为了保持Java代码的兼容性,支持和原生态类型转换,并使用擦除机制实现的泛型。但是使用原生态类型就会失去泛型的优势,会受到编译器警告。
每一条警告都表示可能在运行时抛出ClassCastException异常。要尽最大的努力去消除这些警告。如果无法消除但是可以证明引起警告的代码是安全的,就可以在尽可能小的范围中,使用@SuppressWarnings("unchecked")注解来禁止警告,但是要把禁止的原因记录下来。
参数化类型不支持协变的,即对于任何两个不同的类型Type1和Type2而言,List既不是List的子类型,也不是它的超类。为了解决这个问题,提高灵活性,Java提供了一种特殊的参数化类型,称作有限制的通配符类型,即List? extends E和List? super E。使用原则是producer-extends,consumer-super(PECS)。如果即是生产者,又是消费者,就没有必要使用通配符了。
还有一种特殊的无限制通配符List?,表示某种类型但不确定。常用作泛型的引用,不可向其添加除Null以外的任何对象。
嵌套类(nested class)是指定义在另一个类的内部的类。 嵌套类存在的目的只是为了它的外部类提供服务,如果其他的环境也会用到的话,应该成为一个顶层类(top-level class)。 嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和 局部类(local class)。除了第一种之外,其他三种都称为内部类(inner class)。
总而言之,这四种嵌套类都有自己的用途。假设这个嵌套类属于一个方法的内部,如果只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类。如果一个嵌套类需要在单个方法之外仍然可见,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则就做成静态的。
通过对常见场景的代码逻辑进行抽象封装,形成相应的模板工具类,可以大大减少重复代码,专注于业务逻辑,提高代码质量。
面向对象编程相对于面向过程,多了实例化这一步,而对象的创建必须要指定具体类型。我们常见的做法是“哪里用到,就在哪里创建”,使用实例和创建实例的是同一段代码。这似乎使代码更具有可读性,但是某些情况下造成了不必要的耦合。
对于顶层的(非嵌套的)类和接口,只有两种的访问级别:包级私有的(没有public修饰)和公有的(public修饰)。
对于成员(实例/域、方法、嵌套类和嵌套接口)由四种的访问级别,可访问性如下递增:
正确地使用这些修饰符对于实现信息隐藏是非常关键的,原则就是:尽可能地使每个类和成员不被外界访问(私有或包级私有)。这样好处就是在以后的发行版本中,可以对它进行修改、替换或者删除,而无须担心会影响现有的客户端程序。
不可变类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时提供,并在对象的整个生命周期内固定不变。不可变类好处就是简单易用、线程安全、可自由共享而不容易出错。Java平台类库中包含许多不可变的类,比如String、基本类型包装类、BigDecimal等。
为了使类成为不可变,要遵循下面五条规则:
可变性最小化的一些建议:
TDD的最终目标是整洁可用的代码(clean code that works)。大多数的开发者大部分时间无法得到整洁可用的代码。办法是分而治之。首先解决目标中的“可用”问题,然后再解决“代码的整洁”问题。这与体系结构驱动(architecture-driven)的开发相反。
采用TDD另一个好处就是让我们拥有一套伴随代码产生的详尽的自动化测试集。将来无论出于任何原因(需求、重构、性能改进)需要对代码进行维护时,在这套测试集的驱动下工作,我们代码将会一直是健壮的。
Image [6].png
添加一个测试 - 运行所有测试并检查测试结果 - 编写代码以通过测试 - 运行所有测试且全部通过 - 重构代码,以消除重复设计,优化设计结构
作者:VectorJin
java代码重构最佳实践的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于代码重构方法和经验、java代码重构最佳实践的信息别忘了在本站进行查找喔。







