笔记
《修改代码的艺术》一书中,对如何安全地在现有代码库中修改代码提出了以下步骤:1.定义变更点;2.寻找测试点;3.打破依赖关系;4.编写测试;5.进行修改和重构。
TDD最有价值的一点是,它让我们一次专注于一件事。要么写代码,要么重构;我们从来不会同时做两件事。这种分离在遗留代码中特别有价值,因为它允许我们独立于新代码编写新代码。在我们编写了一些新代码后,我们可以重构以删除它和旧代码之间的任何重复。
在遗留代码中找到bug通常不是问题。就战略而言,它实际上可能是错误的努力。通常,更好的做法是帮助你的团队开始编写一致的正确代码。成功的方法是把精力集中在一开始就不把bug放入代码中。在开发的自然流程中,指定的测试成为保留的测试。你会发现bug,但通常不是第一次运行测试。当你改变了意料之外的行为时,你会在之后的运行中发现bug。
理解代码的方式:
- 笔记/绘图(Notes/Sketching)
通过记笔记或绘制草图来帮助理解代码的结构和逻辑。 - 临时重构(Scratch Refactoring)
从版本控制系统 Check out 到新的分支,不考虑编写测试,随意进行方法提取、变量移动等重构,以便更好地理解代码。但请不要将这些更改提交回版本库,而是在理解完代码后直接丢弃这些修改。这种方法被称为临时重构(Scratch Refactoring)。 - 删除未使用的代码(Delete Unused Code)
如果你发现代码晦涩难懂,并且确定其中的一些代码没有被使用,就删除它。这些代码对你没有任何作用,反而会妨碍你的理解。有些人可能觉得删除代码是一种浪费,毕竟有人曾经花时间写了这些代码,或许未来还能用得上。但这正是版本控制系统的作用——这些代码始终保留在历史版本中。如果以后需要,可以随时找回。
我怎么知道我没有破坏什么?任何可以帮助我们了解我们在输入时如何影响软件的东西,都可以帮助我们减少错误。测试驱动开发在这方面非常强大。 超感知编辑是一种心流状态,在这种状态下,你可以将世界拒之门外,敏感地处理代码。编程是一门一次做一件事的艺术。结对编程:在遗留代码中工作是手术,医生永远不会单独操作。
场景描述
已有一段代码逻辑更新用户信息,但它的代码存在以下问题:
- 缺乏单元测试,无法验证修改是否正确。
- 存在硬编码和强耦合,导致难以扩展和测试。
- 方法过于复杂,多个逻辑混在一起,影响可读性。
原始代码(待修改)
以下是现有的代码逻辑:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void updateUser(User user) {
// 更新用户信息的复杂逻辑
if (user.getId() == null) {
throw new IllegalArgumentException("User ID cannot be null");
}
user.setUpdatedAt(new Date());
userMapper.updateUser(user);
// 发送更新通知
sendUpdateNotification(user);
}
private void sendUpdateNotification(User user) {
System.out.println("Sending update notification for user: " + user.getId());
}
}
第一步:定义变更点
目标是将用户信息更新逻辑优化为更易测试和扩展的实现,同时保留现有行为。
变更点:
updateUser
方法逻辑过于复杂,需要拆分。sendUpdateNotification
方法是不可控的逻辑,需要重构以支持测试。
第二步:寻找测试点
测试点是 updateUser
方法。它的入口是服务层,我们需要编写测试以验证方法的行为。
第三步:打破依赖关系
- 拆分复杂方法:将
updateUser
拆分为多个小方法。 - 引入接口:将通知逻辑抽象为接口,方便测试。
- 使用依赖注入:通过 Spring 的 DI 注入依赖对象。
第四步:编写测试
为确保安全修改,先编写测试以捕获现有行为。
测试前的重构代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private NotificationService notificationService;
public void updateUser(User user) {
validateUser(user);
updateUserDetails(user);
notificationService.sendUpdateNotification(user);
}
private void validateUser(User user) {
if (user.getId() == null) {
throw new IllegalArgumentException("User ID cannot be null");
}
}
private void updateUserDetails(User user) {
user.setUpdatedAt(new Date());
userMapper.updateUser(user);
}
}
public interface NotificationService {
void sendUpdateNotification(User user);
}
@Service
public class NotificationServiceImpl implements NotificationService {
@Override
public void sendUpdateNotification(User user) {
System.out.println("Sending update notification for user: " + user.getId());
}
}
编写单元测试:
public class UserServiceTest {
@InjectMocks
private UserService userService; // 被测试的类
@Mock
private UserMapper userMapper; // Mock 的依赖
@Mock
private NotificationService notificationService; // Mock 的依赖
@Before
public void setUp() {
MockitoAnnotations.initMocks(this); // 初始化 Mock 对象
}
@Test
public void testUpdateUser() {
// 准备测试数据
User user = new User();
user.setId(1L);
user.setName("Test User");
// 执行测试
userService.updateUser(user);
// 验证调用行为
verify(userMapper, times(1)).updateUser(any(User.class));
verify(notificationService, times(1)).sendUpdateNotification(any(User.class));
}
}
第五步:修改和重构
- 添加新的通知功能,例如发送邮件。
- 优化数据库访问逻辑。
重构后的代码:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private NotificationService notificationService;
public void updateUser(User user) {
validateUser(user);
updateUserDetails(user);
notificationService.sendUpdateNotification(user);
}
private void validateUser(User user) {
if (user.getId() == null) {
throw new IllegalArgumentException("User ID cannot be null");
}
}
private void updateUserDetails(User user) {
user.setUpdatedAt(new Date());
userMapper.updateUser(user);
}
}
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void sendUpdateNotification(User user) {
System.out.println("Sending email notification for user: " + user.getId());
}
}
总结
通过以上实践,我们完成了:
- 定义变更点:明确需要优化的
updateUser
方法。 - 寻找测试点:定位服务层逻辑并引入测试。
- 打破依赖关系:拆分方法,抽象接口,并引入依赖注入。
- 编写测试:为原有功能的行为添加单元测试。
- 修改和重构:重构代码以提升可读性和扩展性。
这种方法确保了代码的安全修改,同时保留了现有行为。