您的位置:网站首页 > Java工具 > 正文

常见 Java 代码重构技巧(非常实用)

类别:Java工具 日期:2022-1-15 17:12:33 人气: 来源:

  项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

  编码之前缺乏有效的设计成本上的考虑,在原功能堆砌式编程缺乏有效代码质量监督机制对于此类问题,业界已有有很好的解决思:通过持续不断的重构将代码中的“坏味道”清除掉。

  重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

  大型重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。

  小型重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。什么时候重构 新功能开发、修bug或者代码review中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。

  发散式变化:某个类经常因为不同的原因在不同的方向上发生变化散弹式修改:发生某种变化时,需要在多个类中做修改严重的情结依恋

  两个类、方法签名中包含相同的字段或参数应该使用类但使用基本类型,比如表示数值与币种的Money类、起始值与结束值的Range类不合理的继承体系

  继承打破了封装性,子类依赖其父类中特定功能的实现细节子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明过多的条件判断

  难以复用系统关联性过多,导致很难分离可重用部分难于变化一处变化导致其他很多部分的修改,不利于系统稳定难于理解命名杂乱,结构混乱,难于阅读和理解难以测试分支、依赖较多,难以覆盖全面什么是好代码

  3_代码质量如何衡量.jpg代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

  要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。

  单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少前世死因了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可性。

  添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。

  很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

  父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成。

  调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

  单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展,对修改关闭。

  设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。

  创建型:主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码结构型:主要通过类或对象的不同组合,解耦不同功能的耦合行为型:主要解决的是类或对象之间的交互行为的耦合

  server_main:配置层,负责整个项目的module管理,maven配置管理、资源管理等;server_application:应用接入层,承接外部流量入口,例如:RPC接口实现、消息处理、定时任务等;不要在此包含业务逻辑;server_biz:核心业务层,用例服务、领域实体、领域事件等server_irepository:资源接口层,负责资源接口的server_repository:资源层,负责资源的proxy访问,统一外部资源访问,隔离变化。注意:这里强调的是弱业务性,强数据性;server_common:公共层,vo、工具等代码开发要遵守各层的规范,并注意层级之间的依赖关系。

  一个好的命名应该要满足以下两个约束:准确描述所做得事情格式符合通用的惯例如果你觉得一个类或方法难以命名的时候,可能是其承载的功能太多了,需要进一步拆分。

  类名使用大驼峰命名形式,类命通常使用名词或名词短语。接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如 Cloneable,Callable 等,表示实现该接口的类有某种功能或能力。

  方法命名采用小驼峰的形式,首字小写,往后的每个单词首字母都要大写。和类名不同的是,方法命名一般为动词或动词短语,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。

  多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。

  将函数放进一个单独对象中,如此一来局部变量就变成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。

  不要在convert中调用写操作,避免副作用常见的例外:将查询结果缓存到本地移除不必要临时变量

  把复杂的条件表达式拆分成多个条件表达式,减少嵌套。嵌套了好几层的if - then-else语句,转换为多个if语句

  当存在这样一类条件表达式,它根据对象类型的不同选择不同的行为。可以将这种表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。

  基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将if else(或switch)中的“变化点”封装到子类中。这样,就不需要使用if else(或switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套,比如策略模式、状态模式。

  不要断言,不要使用它来检查“应该”的条件,只使用它来检查“一定必须”的条件如果断言所的约束条件不能满足,代码是否仍能正常运行?如果可以就去掉断言引入Null对象或特殊对象

  当使用一个方法返回的对象时,而这个对象可能为空,这个时候需要对这个对象进行操作前,需要进行判空,否则就会报空指针。当这种判断频繁的出现在各处代码之中,就会影响代码的美观程度和可读性,甚至增加Bug的几率。

  根据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。

  此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。

  继承使实现代码重用的有力手段,但这并非总是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱。与方法调用不同的是,继承打破了封装性。子类依赖于其父类中特定功能的实现细节,如果父类的实现随着发行版本的不同而变化,子类可能会遭到,即使他的代码完全没有改变。

  举例说明,假设有一个程序使用HashSet,为了调优该程序的性能,需要统计HashSet自从它创建以来添加了多少个元素。为了提供该功能,我们编写一个HashSet的变体。

  只有当子类真正是父类的子类型时,才适合继承。对于两个类A和B,只有两者之间确实存在“is-a”关系的时候,类B才应该继承A;在包的内部使用继承常安全的,子类和父类的实现都处在同一个程序员的控制之下;对于专门为了继承而设计并且具有很好的文档说明的类来说,使用继承也常安全的;其他情况就应该优先考虑组合的方式来实现接口优于抽象类

  Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从Java8为接口增加缺省方法(deult method),这两种机制都允许为实例方法提供实现。主要区别在于,为了实现由抽象类定义的类型,类必须称为抽象类的一个子类。因为Java只允许单继承,所以用抽象类作为类型定义受到了。

  现有的类可以很容易被更新,以实现新的接口。接口是定义混合类型(比如Comparable)的理想选择。接口允许构造非层次结构的类型框架。接口虽然提供了缺省方法,但接口仍有有以下局限性:

  接口的变量修饰符只能是public static final的接口的方法修饰符只能是public的接口不存在构造函数,也不存在this可以给现有接口增加缺省方法,但不能确保这些方法在之前存在的实现中都能良好运行。因为这些默认方法是被注入到现有实现中的,它们的实现者并不知道,也没有许可接口缺省方法的设计目的和优势在于:

  Java 8 之前我们知道,一个接口的所有方法其子类必须实现(当然,这个子类不是一个抽象类),但是 java 8 之后接口的默认方法可以选择不实现,如上的操作是可以通过编译期编译的。这样就避免了由 Java 7 升级到 Java 8 时项目编译报错了。Java8在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用lambda。可以减少第三方工具类的创建

  例如在 List 等集合接口中都有一些默认方法,List 接口中默认提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator()等默认方法,这些方法在接口内部创建,避免了为了这些方法而专门去创建相应的工具类。可以避免创建基类

  在 Java 8 之前我们可能需要创建一个基类来实现代码复用,而默认方法的出现,可以不必要去创建基类。由于接口的局限性和设计目的的不同,接口并不能完全替换抽象类。但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法(Template Method)设计模式。

  抽象类AbstractProtocol:封装了服务之后的Exporter和引用服务之后的Invoker实例,并实现了服务的逻辑

  声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口。泛型类和接口统称为泛型(generic type)。泛型从Java 5引入,提供了编译时类型安全检测机制。泛型的本质是参数化类型,通过一个参数来表示所操作的数据类型,并且可以这个参数的类型范围。泛型的好处就是编译期类型检测,避免类型转换。

  由于为了保持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)。

  没有名字,声明的同时进行实例化,只能使用一次。当出现在非静态的中,会持有外部类实例的引用。通常用于创建函数对象和过程对象,不过现在会优先考虑lambda。

  任何可以声明局部变量的地方都可以声明局部类,同时遵循同样的作用域规则。跟匿名类不同的是,有名字可以重复使用。不过实际很少使用局部类。

  最简单的一种嵌套类,声明在另一个类的内部,是这个类的静态,遵循同样的可访问性规则。常见的用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。

  尽管语法上,跟静态类的唯一区别就是类的声明不包含static,但两者有很大的不同。非静态类的每个实例都隐含地与外部类的实例相关联,可以访问外部类的属性和方法。另外必须先创建外部类的实例之后才能创建非静态类的实例。

  总而言之,这四种嵌套类都有自己的用途。假设这个嵌套类属于一个方法的内部,如果只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类。如果一个嵌套类需要在单个方法之外仍然可见,或者它太长了,不适合放在方法内部,就应该使用类。如果类的每个实例都需要一个指向其外围实例的引用,就要把类做成非静态的,否则就做成静态的。

  通过对常见场景的代码逻辑进行抽象封装,形成相应的模板工具类,可以大大减少重复代码,专注于业务逻辑,提高代码质量。

  面向对象编程相对于面向过程,多了实例化这一步,而对象的创建必须要指定具体类型。我们常见的做法是“哪里用到,就在哪里创建”,使用实例和创建实例的是同一段代码。这似乎使代码更具有可读性,但是某些情况下造成了不必要的耦合。

  当对象的类型涉及多态、对象创建复杂(依赖较多)可以考虑将对象的创建过程分离出来,使得使用者不用关注对象的创建细节。设计模式中创建型模式的出发点就是如此,实际项目中可以使用工厂模式、构建器、依赖注入的方式。

  区分一个组件设计得好不好,一个很重要的因素在于,它对于外部组件而言,是否隐藏了其内部数据和实现细节。Java提供了访问控制机制来决定类、接口和的可访问性。实体的可访问性由该实体声明所在的,以及该实体声明中所出现的访问修饰符(private、protected、public)共同决定的。

  对于顶层的(非嵌套的)类和接口,只有两种的访问级别:包级私有的(没有public修饰)和公有的(public修饰)。

  私有的(private修饰)--只有在声明该的顶层类内部才可以访问这个;包级私有的(默认)--声明该的包内部的任何类都可以访问这个;受的(protected修饰)--声明该的类的子类可以访问这个,并且声明该的包内部的任何类也可以访问这个;公有的(public修饰)--在任何地方都可以访问该;正确地使用这些修饰符对于实现信息隐藏常关键的,原则就是:尽可能地使每个类和不被访问(私有或包级私有)。这样好处就是在以后的发行版本中,可以对它进行修改、替换或者删除,而无须担心会影响现有的客户端程序。

  如果类或接口能够做成包级私有的,它就应该被做成包级私有的;如果一个包级私有的顶层类或接口只是在某一个类的内部被用到,就应该考虑使它成为那个类的私有嵌套类;公有类不应直接实例域,应该提供相应的方法以保留将来改变该类的内部表示法的灵活性;当确定了类的公有API之后,应该把其他的都变成私有的;如果同一个包下的类之间存在比较多的访问时,就要考虑重新设计以减少这种耦合;可变性最小化

  不可变类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例时提供,并在对象的整个生命周期内固定不变。不可变类好处就是简单易用、线程安全、可共享而不容易出错。Java平台类库中包含许多不可变的类,比如String、基本类型包装类、BigDecimal等。

  声明所有的域都是私有的声明所有的域都是final的如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个线程,就必须确保正确的行为不提供任何会修改对象状态的方法类不会被扩展(防止子类化,类声明为final)防止粗心或者恶意的子类对象的状态已经改变,从而该类的不可变行为确保对任何可变组件的互斥访问如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在构造器、访问方法和readObject 方法中使用性拷贝技术可变性最小化的一些:

  除非有很好的理由要让类成为可变的类,否则它就应该是不可变的;如果类不能被做成不可变的,仍然应该尽可能地它的可变性;除非有令人信服的理由要使域变成非final的,否则要使每个域都是private final的;构造器应该创建完全初始化的对象,并建立起所有的约束关系;质量如何

  测试驱动开发(TDD)要求以测试作为开发过程的中心,要求在编写任何代码之前,首先编写用于产码行为的测试,而编写的代码又要以使测试通过为目标。TDD要求测试可以完全自动化地运行,并在对代码重构前后必须运行测试。

  TDD的最终目标是整洁可用的代码(clean code that works)。大多数的开发者大部分时间无法得到整洁可用的代码。办法是分而治之。首先解决目标中的“可用”问题,然后再解决“代码的整洁”问题。这与体系结构驱动(architecture-driven)的开发相反。

  采用TDD另一个好处就是让我们拥有一套伴随代码产生的详尽的自动化测试集。将来无论出于任何原因(需求、重构、性能改进)需要对代码进行时,在这套测试集的驱动下工作,我们代码将会一直是健壮的。

  Image [6].png添加一个测试 - 运行所有测试并检查测试结果 - 编写代码以通过测试 - 运行所有测试且全部通过 - 重构代码,以消除重复设计,优化设计结构

  仅在测试失败时才编写代码并且只编写刚好使测试通过的代码编写下一个测试之前消除现有的重复设计,优化设计结构关注点分离是这两条规则隐含的另一个非常重要的原则。其表达的含义指在编码阶段先达到代码“可用”的目标,在重构阶段再追求“整洁”目标,每次只关注一件事!

  重构-改善既有代码的设计设计模式Effective Java敏捷软件开发与设计的最佳实践实现模式测试驱动开发

  

关键词:java 代码重构
0
0
0
0
0
0
0
0
下一篇:没有资料

网友评论 ()条 查看

姓名: 验证码: 看不清楚,换一个

推荐文章更多

热门图文更多

最新文章更多

关于联系我们 - 广告服务 - 友情链接 - 网站地图 - 版权声明 - 人才招聘 - 帮助

郑重声明:本站资源来源网络 如果侵犯了你的利益请联系站长删除

CopyRight 2010-2012 技术支持 FXT All Rights Reserved