Java中如何编写简洁代码?任何傻瓜都能写出让计算机理解的代码。优秀的程序员才会编写出人类能够理解的代码!
1 概述
在本文中,我们将介绍编写简洁代码的原则或约定。我们还将了解为什么简洁的代码很重要,以及如何在Java中实现这一点。此外,我们将找找看是否有什么可用的工具来帮助我们。
2 什么是简洁代码?
在我们开始讨论简洁代码的细节之前,让我们先来了解一下简洁代码是什么意思。其实,并没有一个好的答案能解释这个问题。
在编程中,这个问题涉及面很广,因此产生了一些默认的规则。但是,每种编程语言和范式都呈现出它们自己的一套细微差别,这就要求我们在实践中采用合适的规则。
总的来说,简洁的代码就是任何开发人员都能轻松阅读和修改。虽然听起来像是过度简化了这一概念,但接下来我们将在本文中看到这是如何构建起来的。
Martin Fowler,他曾这样描述简洁代码:
任何傻瓜都能写出让计算机理解的代码。优秀的程序员才会编写出人类能够理解的代码。
3 为什么要关心简洁的代码?
编写简洁的代码是个人习惯问题,但更是个技巧问题。作为一个开发者,我们的经验和知识,日积月累,不断成长。
但是,我们必须要扪心自问,为什么我们要致力于更加简洁的代码呢?很多人可能会觉得这将使得代码更易阅读,但这就足够了吗?
简洁的编码原则更有助于我们达成软件开发原本期望的目标。于此,让我们进一步理解它:
• 更容易维护代码库:任何软件开发的生命周期内,我们都需要对其进行修改和维护,而简洁的代码更易于开发者去修改和维护。
• 更容易排除故障:由于各种各样的原因,软件可能会出现意外的行为。它可能时好时坏,而使用简洁编码原则开发的软件更容易排除问题。
• 更快的上手速度:在软件开发的生命周期中,不同开发人员都会创建、更新和维护它,开发人员可能会在不同的时间点加入。这便需要更快的上手速度来提高效率,而简洁的代码有助于实现这一目标。
4 简洁代码的特征
使用简洁的编码原则编写的代码库有一些特征。我们来看下这些特征:
• 突出重点:编写的代码用来解决特定的问题,严禁去做与它所要解决的问题无关的事情。这适用于代码库中的所有抽象的级别,如方法、类、包或模块。
• 简单:这是简洁代码最重要和也容易被忽略的特性。代码库的复杂性不断增加使它们更容易出错,并且难于阅读和维护。因此,软件的设计和实现必须尽可能的简单,这样才能帮助我们达到预期的效果。
• 可测试:简洁的代码虽然简短,但必须解决紧要的问题。它最好使用自动化的方式能够直观且简单的去测试我们的代码库。这有助于建立代码库的基准行为,并使更改代码库更容易,并且不会破坏任何内容。
这些是帮助我们实现上文所述目标的方法。与后期重构相比,在开发起始考虑这些特征是大有裨益的。这将降低软件生命周期的总体成本。
5 用Java编写简洁的代码
现在我们已经了解了足够多的背景知识,接下来介绍如何在Java中使用那些简洁编码原则。Java提供了许多可以帮助我们编写简洁代码的实例。我们将在不同的应用中对它们进行分类总结,并了解如何使用这些实例编写简洁的代码。
5.1 项目结构
虽然Java不强制任何项目结构,但是遵循一致的模式来组织源文件、测试、配置、数据和其他代码组件是很有用的。Maven是一种流行的Java构建工具,它规定了特定的项目结构。虽然我们可能不使用Maven,但遵守惯例总是有益的。
以下是Maven建议我们创建的一些文件夹:
• src/main/java: 源文件
• src/main/resources:资源文件,比如配置文件
• src/test/java:测试源文件
• src/test/resources:测试资源文件,比如配置文件
5.2. 命名约定
遵循命名约定可以大大提高代码的可读性和可维护性。Spring的创建者Rod Johnson强调了Spring中命名约定的重要性:
“… 如果你知道某个东西做的事情是什么,那么你很有可能猜到它的Spring类或接口的名称 …”
在Java中命名时,规定了一组要遵守的规则。一个格式良好的名称不仅有助于阅读代码,而且还传达了很多关于代码意图的信息。我们来举几个例子:
• 类:在面向对象概念中,类是对象的蓝图(即表现形式),通常表示现实世界的对象。因此,用名词作为类的名称是有意义的:
public class Customer {
}
• 变量: Java中的变量描述了类创建的对象的属性。因此变量的名称应该清楚地描述变量的含义:
public class Customer {
private String customerName;
}
• 方法:Java中的方法是类的一部分,因此通常表示对类创建的对象的属性进行的操作。因此,使用动词来命名方法是很有用的:
public class Customer {
private String customerName;
public String getCustomerName() {
return this.customerName;
}
}
虽然我们只讨论了如何在Java中命名标识符,不过,还有其他一些约定成俗的规则,比如驼峰式大小写,还有很多关于接口、枚举和常量命名的约定,我们都应该注意它们的可读性。
5.3. 源文件结构
源文件中包含不同的元素。虽然Java编译器强制执行一些结构,但大部分都是可变的。不过遵循在源文件中放置元素的特定顺序可以显著提高代码的可读性。可以参考很多流行的风格指南,比如谷歌的和Spring的。
我们看下源文件中元素的典型排序应该是什么样的:
• Package statement //包声明
• Import statements //导入语句
• All static imports //所有静态导入
• All non-static imports //所有非静态导入
• Exactly one top-level class //只有一个顶级类
• Class variables //类变量
• Instance variables //实例变量
• Constructors //构造函数
• Methods //方法
除此之外,还可以根据功能或范围对方法进行分组。虽然没有统一的约定,但我们需要规定一次,然后始终如一地遵循。
让我们来看一个格式良好的源文件:
/src/main/java/com/baeldung/application/entity/Customer.java
package com.baeldung.application.entity;
import java.util.Date;
public class Customer {
private String customerName;
private Date joiningDate;
public Customer(String customerName) {
this.customerName = customerName;
this.joiningDate = new Date();
}
public String getCustomerName() {
return this.customerName;
}
public Date getJoiningDate() {
return this.joiningDate;
}
}
5.4. 空白
众所周知,短段落比大段文字更容易阅读和理解。这同样适用于阅读代码方面,放置良好且一致的空格和空白行可以增强代码的可读性。
我们的思想是在代码中引入逻辑分组,这有助于在阅读代码时思考其过程。这并没有一个单一的规则可以采用,而是一套通用的以可读性为中心的指导方针和指导思想:
• 在静态块、字段、构造函数和内部类之前先空两行
• 方法的签名有多行时在其后空一行
• 保留关键字(如if、for、catch)与其后的圆括号用单个空格隔开
• 保留关键字(如else)和封闭括号(如“{”或“}”)用单个空格隔开
这里列出的例子不多,只是提供一个方向。
5.5. 缩进
虽微不足道,但几乎所有开发人员都认为有良好缩进的代码更容易阅读和理解。Java中没有统一的代码缩进约定。因此,重点就是要么采用一个流行的约定,要么定义一个私有约定,然后整个团队都去遵循它。
让我们来看一些重要的缩进标准:
• 一个典型的标准是使用四个空格来作为一个缩进单元。需注意的是,有些指南建议使用制表符而不是空格作为缩进单元。这两种方式都可以,但关键是要保持一致性!
• 行长度通常都有一个上限,因为现在开发人员使用的屏幕更大,所以这个上限可以设置得比传统的80更高些。
• 最后一点,由于许多表达式都不适合在一行中,我们必须不断分割它们到不同的行中:
• 链式调用方法时.号之前换行
• 在表达式的运算符前换行
• 换行后缩进以提高可读性
让我们来看一个例子:
List<String> customerIds = customer.stream()
.map(customer -> customer.getCustomerId())
.collect(Collectors.toCollection(ArrayList::new));
5.6. 方法参数
对于常规方法来说,参数是必不可少的。但一长串的参数会使人很难阅读和理解代码。那么,我们应该如何去进行约定呢?让我们来了解下可能对我们有帮助的建议:
• 限制一个方法接受的参数数量,最多三个参数是比较好的选择。
• 如果它需要的参数多于推荐的参数时,考虑重构该方法。一般来说,很长的参数列表表明该方法可能要做很多事情。
• 可以将参数绑定到指定的类型中,但注意不要将不相关的参数放到同一个类型中。
• 最后,虽然我们应该使用这些建议来评判代码的可读性,但是我们不能太过死板,要灵活运用。
来看一个例子:
public boolean setCustomerAddress(String firstName, String lastName, String streetAddress,
String city, String zipCode, String state, String country, String phoneNumber) {
}
// This can be refactored as below to increase readability
public boolean setCustomerAddress(Address address) {
}
5.7. 硬编码(固定值)
代码中的硬编码值通常会导致多种副作用。例如,它可能导致重复,从而使修改更加困难。如果需要的值是动态的,通常会导致不良的行为。在大多数情况下,可以通过以下方式之一来替换硬编码值:
• 使用Java中定义的常量或枚举来替换。
• 或者,在类级别或在单独的类文件中定义的常量来替换。
• 如果可能,可以选择配置文件或应用环境中的值来替换。
我们来看一个例子:
private int storeClosureDay = 7;
// This can be refactored to use a constant from Java
private int storeClosureDay = DayOfWeek.SUNDAY.getValue()
同样,也没有严格的准则可以去遵循。但是我们必须知道其他人稍后需要阅读和维护此代码。因此,我们应该选择适合自己的约定,并保持一致。
5.8. 代码注释
在阅读代码以理解琐碎的内容时,代码注释会很有用。同时,注意不要在注释中包含太多显而易见的内容。这可能会使注释臃肿,从而难以阅读。
Java允许两种类型的注释:实现注释和文档注释。它们有不同的目的和格式。让我们来更好地理解它们:
文档/JavaDoc注释
• 受众群体是代码库的使用者
• 细节通常是自由实现的,更关注于规范
• 通常有用且独立于代码库
实现/块注释
• 受众群体是开发代码库的开发人员
• 细节是针对特定场景实现的
• 通常与代码库一起使用
那么,我们怎样才能更好地使用它们,使它们变得有用并与所处环境相关呢?
• 注释应该只是对代码的补充,如果没有注释我们就不能理解代码的话,我们可能需要重构这些代码
• 应该尽量少的使用块注释来描述重要的设计决策
• 对大多数类、接口、公共方法和受保护方法应该使用JavaDoc注释
• 所有的注释都应该格式良好,并有适当的缩进以保证可读性
我们来看一个有意义的文档注释示例:
/**
* This method is intended to add a new address for the customer.
* However do note that it only allows a single address per zip
* code. Hence, this will override any previous address with the
* same postal code.
*
* @param address an address to be added for an existing customer
*/
/*
* This method makes use of the custom implementation of equals
* method to avoid duplication of an address with same zip code.
*/
public addCustomerAddress(Address address) {
}
5.9. 记录日志
任何写代码的人都希望在调试生产代码时获得更多的日志信息。日志的重要性在一般的开发和特殊的维护中怎么强调都不过分。
Java中有很多用于日志记录的库和框架,包括SLF4J、Logback。虽然它们使日志的记录在代码库中变得微不足道,但是必须关注日志记录的最佳方式。否则日志记录模块可能在维护时成为我们的累赘,而不会提供任何帮助。我们来看看其中一些较好的方法:
• 避免过多的日志记录,考虑哪些信息可能有助于故障排除
• 明智地选择日志级别,我们可能希望在生产中选择性地启用不同的日志级别
• 日志消息中的上下文数据要有非常清晰和描述
• 使用外部工具对日志消息进行跟踪、聚合和过滤,以便更快地进行分析
我们来看一个具有正确级别的记录描述性日志的示例:
logger.info(String.format("A new customer has been created with customer Id: %s", id));
6 就这些吗?
尽管上面着重介绍了几种代码格式约定,但这些并不是我们应该了解和关注的唯一约定。可读和可维护的代码受益于日积月累的其他优秀的实践约定。
随着时间的推移,我们可能会遇到一些有趣的缩写词。它们本质上是将学到的知识浓缩为一个或一组原则,这些原则可以帮助我们编写更好的代码。但是,请注意,我们不应该仅仅因为它们存在就遵循它们。很多时候,它们带来的好处与代码库的大小和复杂性成正比。在采用任何原则之前,我们必须审视我们自己的代码库。更重要的是,我们必须让他们保持一致。
6.1. SOLID
SOLID是一个易于记忆的缩写词,它是从编写可理解和可维护的软件的五个原则中提取的:
• 单一责任原则:我们定义的每个接口、类或方法都应该有一个明确的目标,它应该做一件事情并且做好。这对生产出更小的方法和类十分有效,而且它们也是可测试的。
• 开闭原则:理想情况下,我们编写的代码应该可以扩展,而修改是封闭的。意思就是一个类应该以一种不需要修改的方式编写,同时,它允许通过继承或组合的方式进行修改。
• Liskov替换原则:这个原则的意思是每个子类或派生类都可以替代其父类或基类。这有助于减少代码库中的耦合,从而提高代码库跨平台时的可重用性。
• 接口隔离原则:实现接口是一种为类提供特定行为的方法。但是,一个类不需要实现它不需要的方法。需要我们做的是定义更小、更集中的接口。
• 依赖反转原则:根据这个原则,类应该只依赖于抽象,而不依赖于它们的具体实现。这意味着类不应该负责为它们的依赖项创建实例,相反,它们的依赖关系应该注入到类中。
6.2. DRY & KISS
DRY代表Don's Repeat Yourself(不要重复)。该原则指出,不应在整个软件中重复执行一段代码。这个原则背后的基本原理是减少代码重复和增加可重用性。然而,我们不能只从字面意思去理解它。某些复制实际上可以提高代码的可读性和可维护性。
KISS代表“Keep It Simple, Stupid”(笨蛋,保持简单)。这个原则指出,我们应该使代码尽可能简单,这使得它更容易理解和维护。遵循前面提到的一些原则,我们需要保持类和方法的集中和轻量,这将使代码更加简单。
6.3. TDD
TDD代表“Test Driven Development(测试驱动开发)”。这是一种编程思想,要求我们只在自动化测试失败时才编写代码。因此,我们必须从自动化测试的设计开发作为起始点。在Java中,有几个框架可以编写自动化的单元测试,比如JUnit和TestNG。
这样做的好处是巨大的。这使得软件总是按预期工作。由于我们总是从测试开始,所以我们以小块的方式递增地添加工作代码。此外,我们只在新测试或任何旧测试失败时添加代码。这意味着它也会提高可重用性。
7. 辅助工具
编写简洁的代码不仅是原则和实践的问题,而且是个人习惯。随着学习和适应能力的发展,我们往往会成为优秀的开发人员。然而,为了在整个大型团队中保持一致性,我们还必须遵循一些强制措施。
代码审查一直是保持一致性并帮助开发人员通过建设性的反馈意见来成长的好工具。
在Java生态系统中有几种可用的工具,它们承担了代码审阅者的部分职责。让我们来看下这些工具是什么:
• 代码格式化:大多数流行的Java代码编辑器,包括Eclipse和IntelliJ,都支持自动的代码格式化。我们可以使用默认的格式规则定义它们,或者用自定义格式规则替换它们。这涉及到许多结构化代码约定。
• 静态分析工具:有几种用于Java的静态代码分析工具,包括SonarQube、Checkstyle和PMD。它们拥有丰富的规则,可以直接拿来使用,也可以针对特定的项目进行定制。它们在检测违反命名约定和资源泄漏的代码方面非常出色。
8. 总结
在本篇文章中,我们已经讨论了简洁代码所体现的重要的编码原则和特征。我们看到了如何在Java开发中采用这些原则。我们还讨论了有助于保持代码可读性和可维护性的其他最佳方法。最后,我们讨论了一些可用的工具来帮助我们完成这项工作。
总之,需要注意的是,所有这些原则和方法都是为了使我们的代码更简洁。这是一个比较主观的术语,因此必须根据实际场景中的上下文进行评估。
尽管有许多可以采用的规则,但是我们必须认清我们的团队规模、开发习惯和需求。我们可能不得不自定义或为此设计一套全新的规则。无论情况如何,最重要的是在整个团队组织中保持一致,以获得更多益处。
Via:https://www.baeldung.com/java-clean-code
长按打开更多惊喜