Автор Команда Amap_tech.

Вступление

По мере того, как код программного проекта накапливается со временем, затраты на обслуживание системы возрастают. Эта проблема характерна для всех команд разработчиков программного обеспечения. Постоянная оптимизация и улучшение качества кода - отличный способ повысить жизнеспособность системы. Кроме того, как гласит популярная среди разработчиков поговорка: «Думай больше, без кода». Следовательно, нам нужно больше думать при программировании и стремиться улучшить свои навыки программирования. Таким образом, мы можем создавать более элегантный, качественный и эффективный код.

В этой статье будет описан набор правил кодирования, относящихся к функциям Java. Мы надеемся дать несколько советов по программированию для Java-программистов, которые помогут им создать код с вышеупомянутыми особенностями. Отдел сбора данных Amap успешно применяет эти правила на практике.



Использование общих функций инструмента

Дело 1

В следующем фрагменте показано описание симптома.

Неполный код

thisName != null && thisName.equals(name);

Более полный код

(thisName == name) || (thisName != null && thisName.equals(name));

См. Следующее рекомендуемое решение.

Objects.equals(name, thisName);

Случай 2

В следующем фрагменте показано описание симптома.

!(list == null || list.isEmpty());

См. Следующее рекомендуемое решение.

import org.apache.commons.collections4.CollectionUtils;
CollectionUtils.isNotEmpty(list);

Преимущества

  • Функциональное программирование сокращает объем бизнес-кода и интуитивно отображает логику.
  • Общие функции инструмента позволяют нам обдумать логику и снизить вероятность ошибок.

Разделение больших функций

Когда код функции превышает 80 строк, это большая функция, поэтому ее необходимо разделить.

Случай 1) Каждый блок кода должен быть инкапсулирован как функция

Каждый блок кода должен содержать комментарий, объясняющий его функциональность. Если перед блоком кода присутствует строка комментария, она позволяет заменить код функцией и дает имя функции на основе комментария. Если функция имеет описательное имя, ее легко обновить напрямую, не проверяя, как реализован внутренний код.

В следующем фрагменте показано описание симптома.

// 每日生活函数
public void liveDaily() {
    // 吃饭
    // 吃饭相关代码几十行
    // 编码
    // 编码相关代码几十行
    // 睡觉
    // 睡觉相关代码几十行
}

См. Следующее рекомендуемое решение.

// 每日生活函数
public void liveDaily() {
    // 吃饭
    eat();
    // 编码
    code();
    // 睡觉
    sleep();
}
// 吃饭函数
private void eat() {
    // 吃饭相关代码
}
// 编码函数
private void code() {
    // 编码相关代码
}
// 睡觉函数
private void sleep() {
    // 睡觉相关代码
}

Случай 2) Каждое тело цикла должно быть инкапсулировано как функция

В следующем фрагменте показано описание симптома.

// 生活函数
public void live() {
    while (isAlive) {
        // 吃饭
        eat();
        // 编码
        code();
        // 睡觉
        sleep();
    }
}

См. Следующее рекомендуемое решение.

// 生活函数
public void live() {
    while (isAlive) {
        // 每日生活
        liveDaily();
    }
}
// 每日生活函数
private void liveDaily() {
    // 吃饭
    eat();
    // 编码
    code();
    // 睡觉
    sleep();
}

Случай 3) Каждое тело условия должно быть инкапсулировано как функция

В следующем фрагменте показано описание симптома.

// 外出函数
public void goOut() {
    // 判断是否周末
    // 判断是否周末: 是周末则游玩
    if (isWeekday()) {
        // 游玩代码几十行
    }
    // 判断是否周末: 非周末则工作
    else {
        // 工作代码几十行
    }
}

См. Следующее рекомендуемое решение.

// 外出函数
public void goOut() {
    // 判断是否周末
    // 判断是否周末: 是周末则游玩
    if (isWeekday()) {
        play();
    }
    // 判断是否周末: 非周末则工作
    else {
        work();
    }
}
// 游玩函数
private void play() {
    // 游玩代码几十行
}
// 工作函数
private void work() {
    // 工作代码几十行
}

Преимущества

  • Чем короче функция, тем проще ее функционал и дольше ее жизненный цикл.
  • Чем длиннее функция, тем сложнее ее понять и поддерживать. В этом случае обслуживающий персонал постарается по возможности не изменять его.
  • Длинные функции часто содержат повторяющийся код, который трудно найти.

Поддерживайте согласованность уровней кодовых блоков в рамках одной и той же функции

Дело 1

В следующем фрагменте показано описание симптома.

// 每日生活函数
public void liveDaily() {
    // 吃饭
    eat();
    // 编码
    code();
    // 睡觉
    // 睡觉相关代码几十行
}

Как видите, блок кода сна находится не на том же уровне, что и еда и код, что довольно странно. Если вы сравните написание кода с написанием эссе, еда и код - тогда основные идеи абзаца, а сон - это подробный абзац. Для функции LiveDaily вам нужно написать только основной процесс, то есть идею абзаца.

См. Следующее рекомендуемое решение.

public void liveDaily() {
    // 吃饭
    eat();
    // 编码
    code();
    // 睡觉
    sleep();
}
// 睡觉
private void sleep() {
    // 睡觉相关代码
}

Преимущества

  • Вызов функций указывает их цель, тогда как функции реализуют логику выражения. Логика четко организована по уровням для простоты понимания.
  • Блоки кода не обязательно должны быть иерархическими в функции, так как это заставляет функции казаться перегруженными сверху.

Инкапсулируйте идентичный функциональный код как функцию

Случай 1) Инкапсулируйте идентичный код как функцию

В следующем фрагменте показано описание симптома.

// 禁用用户函数
public void disableUser() {
    // 禁用黑名单用户
    List<Long> userIdList = queryBlackUser();
    for (Long userId : userIdList) {
        User userUpdate = new User();
        userUpdate.setId(userId);
        userUpdate.setEnable(Boolean.FALSE);
        userDAO.update(userUpdate);
    }
    // 禁用过期用户
    userIdList = queryExpiredUser();
    for (Long userId : userIdList) {
        User userUpdate = new User();
        userUpdate.setId(userId);
        userUpdate.setEnable(Boolean.FALSE);
        userDAO.update(userUpdate);
    }
}

См. Следующее рекомендуемое решение.

// 禁用用户函数
public void disableUser() {
    // 禁用黑名单用户
    List<Long> userIdList = queryBlackUser();
    for (Long userId : userIdList) {
        disableUser(userId);
    }
    // 禁用过期用户
    userIdList = queryExpiredUser();
    for (Long userId : userIdList) {
        disableUser(userId);
    }
}
// 禁用用户函数
private void disableUser(Long userId) {
    User userUpdate = new User();
    userUpdate.setId(userId);
    userUpdate.setEnable(Boolean.FALSE);
    userDAO.update(userUpdate);
}

Случай 2) Инкапсулируйте аналогичный код как функцию

Инкапсулируйте аналогичный код как функцию, с различиями, контролируемыми параметрами функции.

В следующем фрагменте показано описание симптома.

// 通过工单函数
public void adoptOrder(Long orderId) {
    Order orderUpdate = new Order();
    orderUpdate.setId(orderId);
    orderUpdate.setStatus(OrderStatus.ADOPTED);
    orderUpdate.setAuditTime(new Date());
    orderDAO.update(orderUpdate);
}
// 驳回工单函数
public void rejectOrder(Long orderId) {
    Order orderUpdate = new Order();
    orderUpdate.setId(orderId);
    orderUpdate.setStatus(OrderStatus.REJECTED);
    orderUpdate.setAuditTime(new Date());
    orderDAO.update(orderUpdate);
}

См. Следующее рекомендуемое решение.

// 通过工单函数
public void adoptOrder(Long orderId) {
    auditOrder(orderId, OrderStatus.ADOPTED);
}
// 驳回工单函数
public void rejectOrder(Long orderId) {
    auditOrder(orderId, OrderStatus.REJECTED);
}
// 审核工单函数
private void auditOrder(Long orderId, OrderStatus orderStatus) {
    Order orderUpdate = new Order();
    orderUpdate.setId(orderId);
    orderUpdate.setStatus(orderStatus);
    orderUpdate.setAuditTime(new Date());
    orderDAO.update(orderUpdate);
}

Преимущества

  • Инкапсулируйте общие функции, чтобы уменьшить количество строк кода и улучшить качество кода.
  • Инкапсулируйте общие функции для уточнения бизнес-кода, упрощая его понимание и поддержку.

Инкапсулируйте функции, извлекающие значения параметров

Дело 1

В следующем фрагменте показано описание симптома.

// 是否通过函数
public boolean isPassed(Long userId) {
    // 获取通过阈值
    double thisPassThreshold = PASS_THRESHOLD;
    if (Objects.nonNull(passThreshold)) {
        thisPassThreshold = passThreshold;
    }
    // 获取通过率
    double passRate = getPassRate(userId);
    // 判读是否通过
    return passRate >= thisPassThreshold;
}

См. Следующее рекомендуемое решение.

// 是否通过函数
public boolean isPassed(Long userId) {
    // 获取通过阈值
    double thisPassThreshold = getPassThreshold();
    // 获取通过率
    double passRate = getPassRate(userId);
    // 判读是否通过
    return passRate >= thisPassThreshold;
}
// 获取通过阈值函数
private double getPassThreshold() {
    if (Objects.nonNull(passThreshold)) {
        return passThreshold;
    }
    return PASS_THRESHOLD;
}

Преимущества

  • Значения параметров получаются независимо от бизнес-функций, уточняя бизнес-логику.
  • Инкапсулированная функция значения параметра GET - это независимая функция, которую можно повторно использовать в коде.

Инкапсулируйте идентичную логику с помощью API-интерфейсов на основе параметров

Дело 1

В следующем фрагменте показано описание симптома.

// 发送审核员结算数据函数
public void sendAuditorSettleData() {
    List<WorkerSettleData> settleDataList = auditTaskDAO.statAuditorSettleData();
    for (WorkerSettleData settleData : settleDataList) {
        WorkerPushData pushData = new WorkerPushData();
        pushData.setId(settleData.getWorkerId());
        pushData.setType(WorkerPushDataType.AUDITOR);
        pushData.setData(settleData);
        pushService.push(pushData);
    }
}
// 发送验收员结算数据函数
public void sendCheckerSettleData() {
    List<WorkerSettleData> settleDataList = auditTaskDAO.statCheckerSettleData();
    for (WorkerSettleData settleData : settleDataList) {
        WorkerPushData pushData = new WorkerPushData();
        pushData.setId(settleData.getWorkerId());
        pushData.setType(WorkerPushDataType.CHECKER);
        pushData.setData(settleData);
        pushService.push(pushData);
    }

См. Следующее рекомендуемое решение.

// 发送审核员结算数据函数
public void sendAuditorSettleData() {
    sendWorkerSettleData(WorkerPushDataType.AUDITOR, () -> auditTaskDAO.statAuditorSettleData());
}
// 发送验收员结算数据函数
public void sendCheckerSettleData() {
    sendWorkerSettleData(WorkerPushDataType.CHECKER, () -> auditTaskDAO.statCheckerSettleData());
}
// 发送作业员结算数据函数
public void sendWorkerSettleData(WorkerPushDataType dataType, WorkerSettleDataProvider dataProvider) {
    List<WorkerSettleData> settleDataList = dataProvider.statWorkerSettleData();
    for (WorkerSettleData settleData : settleDataList) {
        WorkerPushData pushData = new WorkerPushData();
        pushData.setId(settleData.getWorkerId());
        pushData.setType(dataType);
        pushData.setData(settleData);
        pushService.push(pushData);
    }
}
// 作业员结算数据提供者接口
private interface WorkerSettleDataProvider {
    // 统计作业员结算数据
    public List<WorkerSettleData> statWorkerSettleData();
}

Преимущества

  • Извлеките основную логику из различных бизнес-функций, чтобы прояснить бизнес-код и упростить его обслуживание.
  • Избегайте многократного написания одного и того же кода. Чем сложнее функция, тем больше польза.

Уменьшите количество уровней функционального кода

Чтобы добиться элегантных функций, мы рекомендуем сохранять количество уровней кода от 1 до 4. Чрезмерные отступы затрудняют чтение функций.

Случай 1) Используйте return, чтобы получить функцию заранее

В следующем фрагменте показано описание симптома.

// 获取用户余额函数
public Double getUserBalance(Long userId) {
    User user = getUser(userId);
    if (Objects.nonNull(user)) {
        UserAccount account = user.getAccount();
        if (Objects.nonNull(account)) {
            return account.getBalance();
        }
    }
    return null;
}

См. Следующее рекомендуемое решение.

// 获取用户余额函数
public Double getUserBalance(Long userId) {
    // 获取用户信息
    User user = getUser(userId);
    if (Objects.isNull(user)) {
        return null;
    }
    // 获取用户账户
    UserAccount account = user.getAccount();
    if (Objects.isNull(account)) {
        return null;
    }
    // 返回账户余额
    return account.getBalance();
}

Случай 2) Используйте continue, чтобы прервать цикл заранее

В следующем фрагменте показано описание симптома.

// 获取合计余额函数
public double getTotalBalance(List<User> userList) {
    // 初始合计余额
    double totalBalance = 0.0D;
    // 依次累加余额
    for (User user : userList) {
        // 获取用户账户
        UserAccount account = user.getAccount();
        if (Objects.nonNull(account)) {
            // 累加用户余额
            Double balance = account.getBalance();
            if (Objects.nonNull(balance)) {
                totalBalance += balance;
            }
        }
    }
    // 返回合计余额
    return totalBalance;
}

См. Следующее рекомендуемое решение.

// 获取合计余额函数
public double getTotalBalance(List<User> userList) {
    // 初始合计余额
    double totalBalance = 0.0D;
    // 依次累加余额
    for (User user : userList) {
        // 获取用户账户
        UserAccount account = user.getAccount();
        if (Objects.isNull(account)) {
            continue;
        }
        // 累加用户余额
        Double balance = account.getBalance();
        if (Objects.nonNull(balance)) {
            totalBalance += balance;
        }
    }
    // 返回合计余额
    return totalBalance;
}

Примечания

Другой метод: в теле цикла вызовите функцию getUserBalance в случае 1, чтобы получить баланс пользователя, а затем рассчитайте совокупный баланс.

В теле цикла мы рекомендуем использовать continue только один раз. Если вы хотите использовать continue несколько раз, мы рекомендуем вам инкапсулировать тело цикла как функцию.

Случай 3) Используйте функцию условного выражения, чтобы уменьшить количество уровней

Для получения дополнительной информации см. «Случай 2: Инкапсулировать сложное условное выражение как функцию» в следующем разделе.

Преимущества

  • Уменьшите количество уровней кода и количество отступов кода.
  • Модули четко разделены для удобства чтения и обслуживания.

Инкапсулировать функции условных выражений

Случай 1) Инкапсулируйте простое условное выражение как функцию

В следующем фрагменте показано описание симптома.

// 获取门票价格函数
public double getTicketPrice(Date currDate) {
    if (Objects.nonNull(currDate) && currDate.after(DISCOUNT_BEGIN_DATE)
        && currDate.before(DISCOUNT_END_DATE)) {
        return TICKET_PRICE * DISCOUNT_RATE;
    }
    return TICKET_PRICE;
}

См. Следующее рекомендуемое решение.

// 获取门票价格函数
public double getTicketPrice(Date currDate) {
    if (isDiscountDate(currDate)) {
        return TICKET_PRICE * DISCOUNT_RATE;
    }
    return TICKET_PRICE;
}
// 是否折扣日期函数
private static boolean isDiscountDate(Date currDate) {
    return Objects.nonNull(currDate) && 
currDate.after(DISCOUNT_BEGIN_DATE)
        && currDate.before(DISCOUNT_END_DATE);
}

Случай 2) Инкапсулируйте сложное условное выражение как функцию

В следующем фрагменте показано описание симптома.

// 获取土豪用户列表
public List<User> getRichUserList(List<User> userList) {
    // 初始土豪用户列表
    List<User> richUserList = new ArrayList<>();
    // 依次查找土豪用户
    for (User user : userList) {
        // 获取用户账户
        UserAccount account = user.getAccount();
        if (Objects.nonNull(account)) {
            // 判断用户余额
            Double balance = account.getBalance();
            if (Objects.nonNull(balance) && balance.compareTo(RICH_THRESHOLD) >= 0) {
                // 添加土豪用户
                richUserList.add(user);
            }
        }
    }
    // 返回土豪用户列表
    return richUserList;
}

См. Следующее рекомендуемое решение.

// 获取土豪用户列表
public List<User> getRichUserList(List<User> userList) {
    // 初始土豪用户列表
    List<User> richUserList = new ArrayList<>();
    // 依次查找土豪用户
    for (User user : userList) {
        // 判断土豪用户
        if (isRichUser(user)) {
            // 添加土豪用户
            richUserList.add(user);
        }
    }
    // 返回土豪用户列表
    return richUserList;
}
// 是否土豪用户
private boolean isRichUser(User user) {
    // 获取用户账户
    UserAccount account = user.getAccount();
    if (Objects.isNull(account)) {
        return false;
    }
    // 获取用户余额
    Double balance = account.getBalance();
    if (Objects.isNull(balance)) {
        return false;
    }
    // 比较用户余额
    return balance.compareTo(RICH_THRESHOLD) >= 0;
}

Предыдущий код также может быть отфильтрован с помощью потокового программирования.

Преимущества

  • Условные выражения получаются независимо от бизнес-функций, поясняя бизнес-логику.
  • Инкапсулированное условное выражение - это независимая функция, которую можно повторно использовать в коде.

Избегайте ненужных суждений о нулевом указателе

Этот раздел относится только к внутреннему коду проекта, с которым вы знакомы. Он демонстрирует, как избежать ненужных суждений о нулевом указателе. Для стороннего промежуточного программного обеспечения и системных API оценки нулевого указателя необходимы для обеспечения устойчивости кода.

Случай 1) Вызов функции, чтобы убедиться, что параметры не равны нулю, чтобы избежать ненужных суждений о нулевом указателе

В следующем фрагменте показано описание симптома.

// 创建用户信息
User user = new User();
... // 赋值用户相关信息
createUser(user);
// 创建用户函数
private void createUser(User user){
    // 判断用户为空
    if(Objects.isNull(user)) {
        return;
    }
    // 创建用户信息
    userDAO.insert(user);
    userRedis.save(user);
}

См. Следующее рекомендуемое решение.

// 创建用户信息
User user = new User();
... // 赋值用户相关信息
createUser(user);
// 创建用户函数
private void createUser(User user){
    // 创建用户信息
    userDAO.insert(user);
    userRedis.save(user);
}

Случай 2) Вызываемая функция гарантирует, что ответ не является нулевым, чтобы избежать ненужных суждений о нулевом указателе

В следующем фрагменте показано описание симптома.

// 保存用户函数
public void saveUser(Long id, String name) {
    // 构建用户信息
    User user = buildUser(id, name);
    if (Objects.isNull(user)) {
        throw new BizRuntimeException("构建用户信息为空");
    }
    // 保存用户信息
    userDAO.insert(user);
    userRedis.save(user);
}
// 构建用户函数
private User buildUser(Long id, String name) {
    User user = new User();
    user.setId(id);
    user.setName(name);
    return user;
}

См. Следующее рекомендуемое решение.

// 保存用户函数
public void saveUser(Long id, String name) {
    // 构建用户信息
    User user = buildUser(id, name);
    // 保存用户信息
    userDAO.insert(user);
    userRedis.save(user);
}
// 构建用户函数
private User buildUser(Long id, String name) {
    User user = new User();
    user.setId(id);
    user.setName(name);
    return user;
}

Случай 3) Логика присвоения значений гарантирует, что данные в списке не являются нулевыми, чтобы избежать ненужных суждений о нулевом указателе.

В следующем фрагменте показано описание симптома.

// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
    return;
}
// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
    UserVO userVo = new UserVO();
    userVo.setId(user.getId());
    userVo.setName(user.getName());
    userVoList.add(userVo);
}
// 依次处理用户
for (UserVO userVo : userVoList) {
    // 判断用户为空
    if (Objects.isNull(userVo)) {
        continue;
    }
    // 处理相关逻辑
    ...
}

См. Следующее рекомендуемое решение.

// 查询用户列表
List<UserDO> userList = userDAO.queryAll();
if (CollectionUtils.isEmpty(userList)) {
    return;
}
// 转化用户列表
List<UserVO> userVoList = new ArrayList<>(userList.size());
for (UserDO user : userList) {
    UserVO userVo = new UserVO();
    userVo.setId(user.getId());
    userVo.setName(user.getName());
    userVoList.add(userVo);
}
// 依次处理用户
for (UserVO userVo : userVoList) {
    // 处理相关逻辑
    ...
}

Случай 4) Возвращенный список и элементы данных функции запроса MyBatis не являются нулевыми, поэтому оценка нулевого указателя не требуется

MyBatis - отличная структура постоянного уровня и компонент промежуточного программного обеспечения базы данных, широко используемый в проектах. Анализируя исходный код MyBatis, вы можете убедиться, что список и элементы данных, возвращаемые функцией запроса, не являются нулевыми, а суждения о нулевом указателе не нужны в коде.

В следующем фрагменте показано описание симптома.

Идея развития хороша, но слишком консервативна.

// 查询用户函数
public List<UserVO> queryUser(Long id, String name) {
    // 查询用户列表
    List<UserDO> userList = userDAO.query(id, name);
    if (Objects.isNull(userList)) {
        return Collections.emptyList();
    }
    // 转化用户列表
    List<UserVO> voList = new ArrayList<>(userList.size());
    for (UserDO user : userList) {
        // 判断对象为空
        if (Objects.isNull(user)) {
            continue;
        }
        // 添加用户信息
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(user, vo);
        voList.add(vo);
    }
    // 返回用户列表
    return voList;
}

См. Следующее рекомендуемое решение.

// 查询用户函数
public List<UserVO> queryUser(Long id, String name) {
    // 查询用户列表
    List<UserDO> userList = userDAO.query(id, name);
    // 转化用户列表
    List<UserVO> voList = new ArrayList<>(userList.size());
    for (UserDO user : userList) {
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(user, vo);
        voList.add(vo);
    }
    // 返回用户列表
    return voList;
}

Преимущества

  • Избегайте ненужных суждений о нулевом указателе, чтобы упростить логику обработки бизнес-кода и повысить эффективность его работы.
  • Эти ненужные суждения о нулевом указателе - это, по сути, мертвый код, который никогда не будет выполнен. Следовательно, их удаление упростит обслуживание кода.

Первоисточник:



Получите доступ к экспертному обзору - Подпишитесь на DDI Intel