本书简单介绍
《代码整洁之道》(Clean Code: A Handbook of Agile Software Craftsmanship)是软件工程师Robert C. Martin(也被称为Uncle Bob)撰写的一本关于软件开发的书籍。这本书自2008年出版以来,成为了软件开发领域内非常有影响力的一本书籍。
主要内容:
- 代码应该易于理解:书中强调了代码应当像散文一样清晰易懂,避免不必要的复杂性和晦涩难懂的技术术语。
- 函数设计:讲解了如何编写短小精悍、单一职责的函数,以及如何命名函数使其意图更加明确。
- 格式化和布局:讨论了代码格式的重要性,包括缩进、空格、换行等,以保持代码的整洁和可读性。
- 对象和数据结构:介绍了面向对象设计原则,如开闭原则、里氏替换原则、依赖倒置原则等,并讨论了如何合理使用数据结构。
- 错误处理:提出了处理错误的几种策略,如使用异常而非返回码,以及如何优雅地处理错误情况。
- 单元测试:强调了单元测试对于确保代码质量的重要性,并提供了编写有效测试的方法。
- 系统设计:探讨了如何通过模块化来构建系统,使系统更加灵活和可维护。
- 并发编程:简要介绍了并发编程的基础知识及其挑战。
目标读者:
这本书适合所有级别的程序员阅读,无论是初学者还是经验丰富的开发者都能从中受益。它不仅提供了具体的编码技巧,还传达了一种对代码质量和软件工艺追求完美的态度。
影响力:
《代码整洁之道》被认为是每个软件开发者的必读书籍之一,因为它不仅仅是一本技术手册,更是一种职业精神的体现。通过学习这本书,开发者可以更好地理解和实践“代码整洁”的理念,从而提高自己的编程水平和项目的整体质量。
知识点详谈
下面是每个知识点的详细解释及其重要性,以及为什么这些原则对编写高质量代码至关重要。
1. 命名
清晰性
为什么重要:
- 可读性:清晰的命名让代码更容易阅读和理解,减少了其他开发者理解代码的时间。
- 维护性:当代码需要修改或扩展时,清晰的命名可以帮助开发者快速定位和理解相关部分。
例子:
// 差的命名
int d = 45; // 这是什么?日期?距离?
// 好的命名
int daysSinceCreation = 45; // 明确表示从创建日起过了多少天
避免误导
为什么重要:
- 准确性:避免误导性的命名可以防止开发者在调用函数或使用变量时产生误解,从而减少错误。
- 一致性:一致的命名约定可以提高代码的整体质量,使代码库更加统一和规范。
例子:
// 差的命名
void calculateDiscount() { // 实际上计算并应用折扣
// 计算折扣
// 应用折扣
}
// 好的命名
void applyDiscount() {
// 计算折扣
// 应用折扣
}
查找友好
为什么重要:
- 可搜索性:好的命名可以通过搜索引擎或IDE的查找功能快速定位到相关的代码片段。
- 效率:提高开发者的效率,减少查找代码的时间。
例子:
// 差的命名
int x1 = 100;
// 好的命名
int width = 100;
避免缩写
为什么重要:
- 可理解性:完整的单词更容易理解,尤其是对于不熟悉项目的新开发者。
- 一致性:避免缩写可以使代码更加一致,减少歧义。
例子:
// 差的命名
int cId = 123; // 客户ID
// 好的命名
int customerId = 123;
2. 函数
单一职责
为什么重要:
- 可测试性:单一职责的函数更容易编写单元测试,因为它们的功能单一且明确。
- 可维护性:当函数只做一件事时,修改和调试变得更加容易。
例子:
// 差的函数
void saveUser(User user) {
// 验证用户信息
// 保存用户到数据库
// 发送确认邮件
}
// 好的函数
void saveUser(User user) {
validateUser(user);
persistUser(user);
sendConfirmationEmail(user);
}
void validateUser(User user) {
// 验证用户信息
}
void persistUser(User user) {
// 保存用户到数据库
}
void sendConfirmationEmail(User user) {
// 发送确认邮件
}
小而专注
为什么重要:
- 可读性:短小的函数更容易阅读和理解。
- 复用性:小而专注的函数可以更容易地在其他地方复用。
例子:
// 差的函数
void processOrder(Order order) {
if (order != null) {
if (order.getStatus() == "PENDING") {
if (order.getAmount() > 0) {
// 处理订单逻辑
}
}
}
}
// 好的函数
void processOrder(Order order) {
if (isValidOrder(order)) {
handleOrder(order);
}
}
boolean isValidOrder(Order order) {
return order != null && order.getStatus().equals("PENDING") && order.getAmount() > 0;
}
void handleOrder(Order order) {
// 处理订单逻辑
}
抽象层次一致
为什么重要:
- 可读性:保持函数内部的抽象层次一致可以使代码更加清晰,避免高抽象和低抽象混杂在一起。
- 可维护性:一致的抽象层次使得代码更容易理解和维护。
例子:
// 差的函数
void processPayment(Payment payment) {
if (payment != null) {
// 高层抽象
updatePaymentStatus(payment, "PROCESSING");
// 低层抽象
String sql = "UPDATE payments SET status = 'PROCESSING' WHERE id = ?";
jdbcTemplate.update(sql, payment.getId());
}
}
// 好的函数
void processPayment(Payment payment) {
if (payment != null) {
updatePaymentStatus(payment, "PROCESSING");
persistPaymentStatus(payment);
}
}
void persistPaymentStatus(Payment payment) {
String sql = "UPDATE payments SET status = ? WHERE id = ?";
jdbcTemplate.update(sql, payment.getStatus(), payment.getId());
}
3. 注释
注释是编写高质量代码的重要组成部分。良好的注释可以帮助其他开发者(包括未来的你自己)更快地理解和维护代码。以下是一些关于如何编写有效注释的最佳实践,以及一些具体的例子。
注释的最佳实践
-
注释的目的
- 解释为什么:注释应该解释为什么要这样做,而不是做了什么。代码本身已经说明了做了什么。
- 解释复杂的逻辑:对于复杂的算法或逻辑,使用注释来解释每一步的目的。
- 解释不直观的代码:对于那些看起来不直观或容易误解的代码,使用注释来澄清意图。
-
注释的格式
- 简洁明了:注释应该简短且直击要点,避免冗长的描述。
- 语法一致:使用一致的注释语法,特别是在团队项目中。
- 更新注释:代码更改时,确保相应的注释也得到更新。
-
注释的类型
- 行内注释:在同一行代码的末尾添加注释,用于解释单行代码。
- 块注释:用于解释多行代码或复杂的逻辑。
- 文档注释:用于生成API文档,通常出现在类、方法和字段的前面。
具体例子
行内注释
public class Calculator {
public int add(int a, int b) {
return a + b; // 返回两个整数的和
}
}
块注释
public class Calculator {
/**
* 计算两个整数的和。
*
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的和
*/
public int add(int a, int b) {
return a + b;
}
/**
* 计算两个整数的乘积。
* 如果任何一个输入为0,则返回0。
*
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的乘积
*/
public int multiply(int a, int b) {
if (a == 0 || b == 0) { // 如果任意一个数为0,直接返回0
return 0;
}
return a * b;
}
}
文档注释
/**
* 计算器类,提供基本的数学运算功能。
*/
public class Calculator {
/**
* 计算两个整数的和。
*
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的和
*/
public int add(int a, int b) {
return a + b;
}
/**
* 计算两个整数的乘积。
* 如果任何一个输入为0,则返回0。
*
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的乘积
*/
public int multiply(int a, int b) {
if (a == 0 || b == 0) { // 如果任意一个数为0,直接返回0
return 0;
}
return a * b;
}
}
特殊情况下的注释
- 复杂的算法
/**
* 使用快速排序算法对数组进行排序。
*
* @param arr 要排序的数组
* @param low 起始索引
* @param high 结束索引
*/
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high); // 找到枢轴位置
quickSort(arr, low, pivotIndex - 1); // 递归排序左半部分
quickSort(arr, pivotIndex + 1, high); // 递归排序右半部分
}
}
/**
* 分区函数,用于快速排序。
*
* @param arr 要分区的数组
* @param low 起始索引
* @param high 结束索引
* @return 枢轴位置
*/
private int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为枢轴
int i = (low - 1); // 指针i指向小于枢轴的最后一个元素
for (int j = low; j < high; j++) {
if (arr[j] < pivot) { // 如果当前元素小于枢轴
i++; // 移动指针i
swap(arr, i, j); // 交换元素
}
}
swap(arr, i + 1, high); // 将枢轴放到正确的位置
return i + 1; // 返回枢轴位置
}
/**
* 交换数组中的两个元素。
*
* @param arr 数组
* @param i 第一个元素的索引
* @param j 第二个元素的索引
*/
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 不直观的代码
public class StringUtil {
/**
* 检查字符串是否为空或仅包含空白字符。
*
* @param str 要检查的字符串
* @return 如果字符串为空或仅包含空白字符,则返回true,否则返回false
*/
public static boolean isBlank(String str) {
if (str == null || str.trim().isEmpty()) { // 检查字符串是否为空或仅包含空白字符
return true;
}
return false;
}
}
总结
良好的注释应该:
- 解释代码背后的逻辑和目的,而不仅仅是代码本身。
- 使用一致的格式和语法。
- 在代码更改时及时更新。
- 避免冗余和不必要的注释。
通过遵循这些最佳实践,可以使代码更加清晰、易于理解和维护。
4. 格式化
代码格式化是编写高质量代码的重要环节。良好的代码格式不仅提高了代码的可读性,还使得团队协作更加顺畅。大多数现代IDE(如IntelliJ IDEA、Eclipse、Visual Studio Code等)都内置了代码格式化的工具,也可以通过插件来增强这些功能。下面是一些常见的代码格式化规则和最佳实践,以及如何在不同环境中应用这些规则。
常见的代码格式化规则
-
缩进
- 缩进风格:常见的缩进风格有K&R风格(使用大括号放在同一行)和Allman风格(使用大括号放在下一行)。
- 缩进大小:通常使用4个空格或一个Tab键。建议团队统一使用一种缩进方式。
-
行宽
- 最大行宽:通常设置为80或120个字符,超过这个长度的行应该进行换行。
-
空格
- 关键字后:在关键字(如
if
、for
、while
等)和括号之间加一个空格。 - 运算符周围:在运算符(如
+
、-
、=
等)前后加一个空格。 - 逗号后:在逗号后面加一个空格。
- 关键字后:在关键字(如
-
大括号
- 始终使用大括号:即使只有一行代码,也应该使用大括号,以避免潜在的错误。
- 大括号位置:可以选择K&R风格或Allman风格,但团队内部要保持一致。
-
命名规范
- 变量名:使用驼峰命名法(camelCase),如
studentName
。 - 常量名:使用全大写,单词间用下划线分隔,如
MAX_VALUE
。 - 类名:使用帕斯卡命名法(PascalCase),如
StudentClass
。
- 变量名:使用驼峰命名法(camelCase),如
-
注释
- 行内注释:注释前至少留两个空格。
- 块注释:注释应该清晰、简洁,解释代码的目的和逻辑。
-
导入语句
- 排序:导入语句按字母顺序排列。
- 分组:将标准库、第三方库和项目内部库分别分组,并用空行隔开。
示例代码
不格式化的代码
public class Calculator{
public int add(int a,int b){return a+b;}
public int subtract(int a,int b){return a-b;}
public int multiply(int a,int b){return a*b;}
public int divide(int a,int b){
if(b==0){
throw new IllegalArgumentException("除数不能为零");
}return a/b;
}}
格式化的代码
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("除数不能为零");
}
return a / b;
}
}
使用工具进行代码格式化
IntelliJ IDEA
- 自动格式化:
- 选择代码块,然后按
Ctrl + Alt + L
(Windows/Linux)或Cmd + Option + L
(Mac)。
- 选择代码块,然后按
- 配置格式化规则:
- 进入
File
->Settings
->Editor
->Code Style
,可以自定义各种格式化规则。
- 进入
Eclipse
- 自动格式化:
- 选择代码块,然后按
Ctrl + Shift + F
。
- 选择代码块,然后按
- 配置格式化规则:
- 进入
Window
->Preferences
->Java
->Code Style
->Formatter
,可以自定义格式化规则。
- 进入
Visual Studio Code
- 安装插件:
- 安装
Prettier
或ESLint
等插件。
- 安装
- 自动格式化:
- 保存文件时自动格式化,或者手动选择代码块,然后按
Shift + Alt + F
。
- 保存文件时自动格式化,或者手动选择代码块,然后按
- 配置格式化规则:
- 在
.vscode
目录下创建settings.json
文件,配置格式化规则。
- 在
总结
良好的代码格式化应该遵循以下原则:
- 一致性:团队内部应该使用一致的格式化规则。
- 可读性:代码应该清晰、易读,避免冗余和混乱。
- 自动化:利用IDE和插件的自动化功能,减少手动格式化的工作量。
通过遵循这些最佳实践,可以编写出更加整洁、易维护的代码。
5. 对象和数据结构
为什么重要:
- 封装性:对象可以封装数据和行为,提供更好的抽象和封装。
- 灵活性:对象可以通过继承和多态提供更高的灵活性和扩展性。
例子:
下面我将用Java语言来展示如何使对象和数据结构更加整洁。
对象的整洁示例
不整洁的示例
假设我们有一个Book
类,用来表示一本书的信息,同时也包含了处理借阅和归还书籍的功能。
public class Book {
private String isbn;
private String title;
private String author;
private int publicationYear;
private boolean isBorrowed;
public Book(String isbn, String title, String author, int publicationYear) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.isBorrowed = false; // 默认书籍未被借出
}
public void borrowBook() {
if (!isBorrowed) {
isBorrowed = true;
System.out.println("书已成功借出");
} else {
System.out.println("该书已被借出");
}
}
public void returnBook() {
if (isBorrowed) {
isBorrowed = false;
System.out.println("书已成功归还");
} else {
System.out.println("该书尚未被借出");
}
}
// Getters and Setters
}
在这个例子中,Book
类既负责存储书籍信息,又负责处理借阅和归还逻辑,违反了单一职责原则。此外,控制台输出的方式也不适合大型系统,因为这通常需要更复杂的日志记录。
整洁的示例
我们可以创建一个新的类LibraryService
来处理借阅和归还的逻辑,同时使用异常来处理错误情况。
public class Book {
private String isbn;
private String title;
private String author;
private int publicationYear;
private boolean isBorrowed;
public Book(String isbn, String title, String author, int publicationYear) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.isBorrowed = false; // 默认书籍未被借出
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public int getPublicationYear() {
return publicationYear;
}
public boolean isBorrowed() {
return isBorrowed;
}
public void setBorrowed(boolean borrowed) {
isBorrowed = borrowed;
}
}
public class LibraryService {
private Map<String, Book> books = new HashMap<>();
public void addBook(Book book) {
books.put(book.getIsbn(), book);
}
public void borrowBook(String isbn) throws BookNotFoundException, BookAlreadyBorrowedException {
Book book = books.get(isbn);
if (book == null) {
throw new BookNotFoundException("找不到ISBN为 " + isbn + " 的书");
}
if (book.isBorrowed()) {
throw new BookAlreadyBorrowedException("书已借出");
}
book.setBorrowed(true);
}
public void returnBook(String isbn) throws BookNotFoundException, BookNotBorrowedException {
Book book = books.get(isbn);
if (book == null) {
throw new BookNotFoundException("找不到ISBN为 " + isbn + " 的书");
}
if (!book.isBorrowed()) {
throw new BookNotBorrowedException("书未被借出");
}
book.setBorrowed(false);
}
}
// 自定义异常类
public class BookNotFoundException extends Exception {
public BookNotFoundException(String message) {
super(message);
}
}
public class BookAlreadyBorrowedException extends Exception {
public BookAlreadyBorrowedException(String message) {
super(message);
}
}
public class BookNotBorrowedException extends Exception {
public BookNotBorrowedException(String message) {
super(message);
}
}
在这个整洁的例子中:
Book
类只负责存储书籍信息,不再包含借阅和归还的逻辑。LibraryService
类负责管理书籍的借阅和归还操作。- 使用异常来处理错误情况,使代码更加健壮和灵活。
- 分离关注点,使得代码更容易测试和维护。
数据结构的整洁示例
假设我们需要存储用户的购物车信息,包括用户ID、商品列表及其数量。
不整洁的示例
public class ShoppingCart {
private String userId;
private List<Item> items;
public ShoppingCart(String userId) {
this.userId = userId;
this.items = new ArrayList<>();
}
public void addItem(String productId, int quantity) {
Item newItem = new Item(productId, quantity);
items.add(newItem);
}
public void removeItem(String productId) {
items.removeIf(item -> item.getProductId().equals(productId));
}
public List<Item> getItems() {
return items;
}
}
public class Item {
private String productId;
private int quantity;
public Item(String productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
public String getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
}
这个例子中,ShoppingCart
类直接使用了一个简单的列表来存储商品信息,没有考虑到商品可能重复添加的情况,也没有提供方便的方法来更新商品数量。
整洁的示例
我们可以使用一个映射来存储商品ID和数量的关系,以便更有效地管理购物车中的商品。
import java.util.HashMap;
import java.util.Map;
public class ShoppingCart {
private String userId;
private Map<String, Integer> items;
public ShoppingCart(String userId) {
this.userId = userId;
this.items = new HashMap<>();
}
public void addItem(String productId, int quantity) {
items.merge(productId, quantity, Integer::sum);
}
public void removeItem(String productId) {
items.remove(productId);
}
public void updateItemQuantity(String productId, int newQuantity) {
if (newQuantity <= 0) {
items.remove(productId);
} else {
items.put(productId, newQuantity);
}
}
public Map<String, Integer> getItems() {
return items;
}
}
在这个整洁的例子中:
- 使用
HashMap
来存储商品ID和数量的关系,避免了商品重复添加的问题。 - 提供了
updateItemQuantity
方法来更新商品的数量,使得操作更加直观和高效。 - 代码更加简洁,易于理解和维护。
6. 错误处理
为什么重要:
- 健壮性:良好的错误处理可以提高程序的健壮性,使其在遇到异常情况时能够优雅地处理。
- 可维护性:明确的错误处理逻辑使代码更容易理解和维护。
良好的错误处理应该包括以下几个方面:
- 异常处理:使用try-catch块捕获和处理异常,必要时抛出自定义异常。
- 日志记录:记录详细的错误信息和上下文,便于后续分析和调试。
- 友好的用户提示:向用户提供易于理解的错误提示,而不是技术性的错误信息。
- API错误响应:在API开发中,返回适当的HTTP状态码和自定义错误码,以便客户端更好地处理错误。
例子:
错误处理是编写健壮软件的关键部分。良好的错误处理不仅可以提高用户体验,还可以帮助开发者更快地定位和解决问题。以下是几种常见的错误处理策略和最佳实践,以及如何在Java中实现它们。
常见错误处理策略
-
异常处理
- 捕获异常:使用
try-catch
块来捕获和处理可能出现的异常。 - 抛出异常:在遇到无法处理的错误时,抛出异常给调用者处理。
- 自定义异常:创建自定义异常类,以便更好地描述特定的错误情况。
- 捕获异常:使用
-
日志记录
- 记录错误信息:使用日志框架(如Log4j、SLF4J等)记录错误信息,便于后续分析和调试。
- 记录上下文信息:除了错误信息外,还应记录发生错误时的上下文信息,如用户ID、请求参数等。
-
错误响应
- 友好的用户提示:向用户提供友好的错误提示,而不是显示技术性的错误信息。
- 状态码和错误码:在API开发中,返回适当的HTTP状态码和自定义错误码,以便客户端更好地处理错误。
-
事务管理
- 回滚事务:在数据库操作中,如果发生错误,应回滚事务,以保持数据的一致性。
Java中的错误处理示例
异常处理
假设我们有一个方法用于从数据库中查询用户信息,如果用户不存在,则抛出自定义异常。
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
public class UserService {
public User getUserById(int userId) throws UserNotFoundException {
User user = userRepository.findById(userId);
if (user == null) {
throw new UserNotFoundException("用户ID为 " + userId + " 的用户不存在");
}
return user;
}
}
捕获异常
在调用上述方法时,可以使用try-catch
块来捕获并处理异常。
public class UserController {
private UserService userService;
public void getUser(int userId) {
try {
User user = userService.getUserById(userId);
System.out.println("找到用户: " + user.getName());
} catch (UserNotFoundException e) {
System.out.println(e.getMessage());
}
}
}
日志记录
使用日志框架记录错误信息。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User getUserById(int userId) throws UserNotFoundException {
try {
User user = userRepository.findById(userId);
if (user == null) {
throw new UserNotFoundException("用户ID为 " + userId + " 的用户不存在");
}
return user;
} catch (UserNotFoundException e) {
logger.error("用户查询失败", e);
throw e;
}
}
}
友好的用户提示
在控制器层中,可以将异常转换为友好的用户提示。
public class UserController {
private UserService userService;
public void getUser(int userId) {
try {
User user = userService.getUserById(userId);
System.out.println("找到用户: " + user.getName());
} catch (UserNotFoundException e) {
System.out.println("对不起,未找到该用户。请检查用户ID是否正确。");
logger.error("用户查询失败", e);
}
}
}
API错误响应
在RESTful API中,返回适当的HTTP状态码和错误信息。
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private UserService userService;
@GetMapping("/users/{userId}")
public ResponseEntity<User> getUser(@PathVariable int userId) {
try {
User user = userService.getUserById(userId);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
logger.error("用户查询失败", e);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
class ErrorResponse {
private int status;
private String message;
public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
// Getters and Setters
}
总结
良好的错误处理应该包括以下几个方面:
- 异常处理:使用
try-catch
块捕获和处理异常,必要时抛出自定义异常。 - 日志记录:记录详细的错误信息和上下文,便于后续分析和调试。
- 友好的用户提示:向用户提供易于理解的错误提示,而不是技术性的错误信息。
- API错误响应:在API开发中,返回适当的HTTP状态码和自定义错误码,以便客户端更好地处理错误。
通过这些最佳实践,可以编写出更加健壮和可靠的软件。
7. 边界
为什么重要:
- 解耦:明确的边界设计可以减少模块之间的耦合,使系统更加模块化和可维护。
- 扩展性:清晰的边界使得系统更容易扩展和修改,而不会影响其他部分。
例子:
// 差的边界设计
public class OrderService {
public void placeOrder(Order order) {
// 业务逻辑
// 数据库操作
// 第三方服务调用
}
}
// 好的边界设计
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
public void placeOrder(Order order) {
// 业务逻辑
orderRepository.save(order);
paymentGateway.charge(order.getPayment());
}
}
8. 单元测试
为什么重要:
- 正确性:单元测试可以验证代码的正确性,确保功能按预期工作。
- 可维护性:单元测试可以在修改代码后快速发现回归问题,提高代码的可维护性。
- 设计:编写单元测试的过程可以促使开发者思考代码的设计和结构,从而写出更好的代码。
例子:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Test
void testFindUserById() {
UserService userService = new UserService();
User user = userService.findUserById(1);
assertNotNull(user);
assertEquals("John Doe", user.getName());
}
}
通过这些详细的解释和例子,希望能帮助你更好地理解和应用《代码整洁之道》中的原则,从而编写出更加高质量的代码。