代码工艺:实践《修改代码的艺术》中如何安全地在现有代码库中修改代码的方法

笔记

《修改代码的艺术》一书中,对如何安全地在现有代码库中修改代码提出了以下步骤:1.定义变更点;2.寻找测试点;3.打破依赖关系;4.编写测试;5.进行修改和重构。

TDD最有价值的一点是,它让我们一次专注于一件事。要么写代码,要么重构;我们从来不会同时做两件事。这种分离在遗留代码中特别有价值,因为它允许我们独立于新代码编写新代码。在我们编写了一些新代码后,我们可以重构以删除它和旧代码之间的任何重复。

在遗留代码中找到bug通常不是问题。就战略而言,它实际上可能是错误的努力。通常,更好的做法是帮助你的团队开始编写一致的正确代码。成功的方法是把精力集中在一开始就不把bug放入代码中。在开发的自然流程中,指定的测试成为保留的测试。你会发现bug,但通常不是第一次运行测试。当你改变了意料之外的行为时,你会在之后的运行中发现bug。

理解代码的方式:

  • 笔记/绘图(Notes/Sketching)
    通过记笔记或绘制草图来帮助理解代码的结构和逻辑。
  • 临时重构(Scratch Refactoring)
    从版本控制系统 Check out 到新的分支,不考虑编写测试,随意进行方法提取、变量移动等重构,以便更好地理解代码。但请不要将这些更改提交回版本库,而是在理解完代码后直接丢弃这些修改。这种方法被称为临时重构(Scratch Refactoring)。
  • 删除未使用的代码(Delete Unused Code)
    如果你发现代码晦涩难懂,并且确定其中的一些代码没有被使用,就删除它。这些代码对你没有任何作用,反而会妨碍你的理解。有些人可能觉得删除代码是一种浪费,毕竟有人曾经花时间写了这些代码,或许未来还能用得上。但这正是版本控制系统的作用——这些代码始终保留在历史版本中。如果以后需要,可以随时找回。

我怎么知道我没有破坏什么?任何可以帮助我们了解我们在输入时如何影响软件的东西,都可以帮助我们减少错误。测试驱动开发在这方面非常强大。 超感知编辑是一种心流状态,在这种状态下,你可以将世界拒之门外,敏感地处理代码。编程是一门一次做一件事的艺术。结对编程:在遗留代码中工作是手术,医生永远不会单独操作。

场景描述

已有一段代码逻辑更新用户信息,但它的代码存在以下问题:

  1. 缺乏单元测试,无法验证修改是否正确。
  2. 存在硬编码和强耦合,导致难以扩展和测试。
  3. 方法过于复杂,多个逻辑混在一起,影响可读性。

原始代码(待修改)

以下是现有的代码逻辑:

@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());
    }
}

第一步:定义变更点

目标是将用户信息更新逻辑优化为更易测试和扩展的实现,同时保留现有行为。

变更点:

  1. updateUser 方法逻辑过于复杂,需要拆分。
  2. sendUpdateNotification 方法是不可控的逻辑,需要重构以支持测试。

第二步:寻找测试点

测试点updateUser 方法。它的入口是服务层,我们需要编写测试以验证方法的行为。

第三步:打破依赖关系

  1. 拆分复杂方法:将 updateUser 拆分为多个小方法。
  2. 引入接口:将通知逻辑抽象为接口,方便测试。
  3. 使用依赖注入:通过 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));
    }
}

第五步:修改和重构

  1. 添加新的通知功能,例如发送邮件。
  2. 优化数据库访问逻辑。

重构后的代码:

@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());
    }
}

总结

通过以上实践,我们完成了:

  1. 定义变更点:明确需要优化的 updateUser 方法。
  2. 寻找测试点:定位服务层逻辑并引入测试。
  3. 打破依赖关系:拆分方法,抽象接口,并引入依赖注入。
  4. 编写测试:为原有功能的行为添加单元测试。
  5. 修改和重构:重构代码以提升可读性和扩展性。

这种方法确保了代码的安全修改,同时保留了现有行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值