实现代码优雅的十条心法

开发
本文总结了 Code Review 对开发效率、代码整洁上的影响,内容涵盖了一些设计的思想,也提供了一些常用的代码片段,非常实用。

近期在项目中看了自己先前写过的代码,感触最多的就是 Code Review 对开发效率、代码整洁上的影响。这里总结下,内容涵盖了一些设计的思想,也提供了一些常用的代码片段,非常实用。

开篇导读

基础版

1.清晰的注释

比如:在一个业务类中创建订单接口,

执行流程:下订单 -> 减库存-> 支付-> 日志。通过合理的注释可以获得清晰的执行逻辑。当然,可在类、方法、属性上使用注释,但是注释只是一个说明性的东西,能表达意思即可,不易过度使用。

/**
 * 订单业务实现类
 */
public class OrderService {
    
    
    @Autowired
    InventoryService  inventoryService;

    @Autowired
    PaymentService  paymentService;

    @Autowired
    LogService  logService;

    /**
     * 创建订单
     * 执行下订单、减库存、支付处理和记录日志的流程。
     */
    public String createOrder(Order order) {

        // 检查订单
        if (!order.isValid()) {
            return "订单无效";
        }

        // 减库存
        if (!inventoryService.decreaseStock(order.getProduct(), order.getQuantity())) {
            return "库存不足";
        }

        // 支付
        if (!paymentService.process(order.getPaymentInfo())) {
            // 如果支付失败,需要回滚库存
            inventoryService.increaseStock(order.getProduct(), order.getQuantity());
            return "支付失败";
        }

        // 订单创建成功,记录日志
        logService.createOrderLog(order);

        // 返回订单创建成功的消息
        return "订单创建成功";
    }
}

2.提高可读性

可读性较差的版本:可以看到业务逻辑混在一起,可读性非常差。

 public String process(int id, int num, double price) {
        if (id > 0 && num > 0 && price > 0) {
            Product p = productRepository.getProductById(id);
            if (p != null && p.getStock() >= num) {
                boolean isPaymentSuccess = paymentService.pay(price);
                if (isPaymentSuccess) {
                    productRepository.updateStock(id, p.getStock() - num);
                    return "Order processed";
                }
            }
            return "Error";
        }
        return "Invalid param";
    }

可读性较好的版本:在于方法的精细化抽象。清晰的命名和表达。如:(1)小节的实现。

3.命名规范

(1) 见名知意

比如,有个方法是校验逻辑,

正例:

   //清晰的命名,见名知义
   void validOrder(Order order);

反例:

   //命名随意,写时一时爽,日后莫相忘
   void methodA();

(2) 常量命名

常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚。

public class BaseInfo {
 /**
  * 业务标识
  */
 public final static String BIZ_CODE = "XXXXXX";
    
}

(3) 峰命名

方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。

正例:

getHttpMessage() 

(4) 实体类命名 :VO DO DTO POJO

使用场景:

  • POJO:作为简单的数据结构,不包含业务逻辑。
  • DO:在业务逻辑层使用,代表业务实体。
  • DTO:在需要将数据从一个层传输到另一个层,或者在远程调用中使用。
  • VO:在展示层使用,用于将数据展示给用户。

4.使用轮子

Java类库中有很多常用的方法,也有一些开源的工具类等供使用。下面句几个例子:

(1) Lambda表达式

主要针对函数式接口,简化代码使用。不过使用过程中注意空指针的一些问题

案例:统计北京地区(按区划分) ,年龄在25~30 岁的,月薪超过20k 的人群 中薪资最高的程序员 组合成一个Map, 地区为key , 每个value 是一个List. 用Lambda 实现

public static void main(String[] args) {
        // 模拟的程序员列表
        List<Developer> developers = new ArrayList<>();
        developers.add(new Developer("Alice", "北京朝阳区", 28, 22000, "男"));
        developers.add(new Developer("Alice", "北京朝阳区", 25, 28000, "男"));
        developers.add(new Developer("Bob", "北京海淀区", 24, 23000, "男"));
        developers.add(new Developer("Bob", "北京海淀区", 29, 30000, "男"));
        developers.add(new Developer("Charlie", "北京朝阳区", 31, 25000, "男"));


        // 根据条件过滤程序员,并按地区分组,每个地区选择薪资最高的程序员
        Map<String, Optional<Developer>> map = developers.stream()
                .filter(d -> d.getLocation().startsWith("北京")) // 北京地区
                .filter(d -> d.getAge() >= 25 && d.getAge() <= 30) // 年龄在25~30岁
                .filter(d -> d.getSalary() > 20000) // 月薪超过20k
                .collect(Collectors.groupingBy(Developer::getLocation,
                        // 对每个地区的程序员按薪资进行排序,选择薪资最高的程序员
                        LinkedHashMap::new, // 保持插入顺序,有助于选择薪资最高的
                        Collectors.maxBy(Comparator.comparing(Developer::getSalary))));


        // 运行
        map.forEach((district, highestPaidDev) -> {
            System.out.println("地区: " + district);
            highestPaidDev.ifPresent(
                    dev -> System.out.println("薪资最高的程序员: " + dev)
            );
        });

    }
    
    
// 运行结果:

// 地区: 北京朝阳区
// 薪资最高的程序员: Developer{name='Alice', location='北京朝阳区', age=25, salary=28000.0, gender='男'}
//地区: 北京海淀区
//薪资最高的程序员: Developer{name='Bob', location='北京海淀区', age=29, salary=30000.0, gender='男'}

(2) 使用Lombook

主要用于一些实体类,简化代码:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Orders {
    private Integer id;
    private Integer orderType;
    private Integer customerId;
    private Double amount;
    private Integer status;
    private Date createDate;
    private Date updateDate;


   //省略了 构造函数、getter,setter方法等
}

当然使用过程可能存在一些问题,读者自行摸索,但是从简化的角度讲,确实显得干净整洁。

(3) 使用工具类Hutool

Hutool提供了很多好用的工具类,也针对其做了大量封装。用于日常业务开发。

官方文档:Hutool参考文档

例如: 日期时间偏移 日期或时间的偏移指针对某个日期增加或减少分、小时、天等等,达到日期变更的目的。

String dateStr = "2017-03-01 22:33:23";

//结果:2017-03-03 22:33:23
Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);

5.优雅的异常处理

异常用来定位问题使用,常见的异常处理 是在业务方法里捕获。

  try {
     // 业务逻辑
  } catch (Exception e) {
      log.error("业务逻辑异常:{}", e.fillInStackTrace());
  } finally {
      // 不管是否发生异常,finally块中的代码都会执行
  }

实际上,可以通过全局异常处理方式实现。大致思路如, 使用细节请自行搜索。

/**
 * @ClassName GlobalExceptionHandler
 * @Author weiweiyixiao
 * @Description @ControllerAdvice + @ExceptionHandler 实现全局的 Controller 层的异常处理
 * @Date 2024/7/4 22:53
 * @Version 1.0
 */

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 处理自定义异常
     */
    @ExceptionHandler(value = DefinitionException.class)
    @ResponseBody
    public Result bizExceptionHandler(DefinitionException e) {
        return Result.defineError(e);
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result exceptionHandler(Exception e) {
        return Result.otherError(ErrorEnum.INTERNAL_SERVER_ERROR);
    }

}

进阶版

到了这一章,我们从整体上考量程序的设计的健壮性、拓展性。常见的有方案:AOP思想、代码分层、公共抽离、设计模式等。

6.AOP思想

AOP思想 是程序解耦的一种重要手段,常用来记录日志,

// 定义一个切面类
@Aspect
@Component
public class LoggingAspect {

    // 定义一个切入点,这里以所有以"handle"开头的方法为例
    @Pointcut("execution(* com.yourpackage.service.*.handle*(..))")
    private void handleMethod() {}

    // 在方法执行之前记录日志
    @Before("handleMethod()")
    public void logBefore(JoinPoint joinPoint) {
        // 获取被拦截的方法名
        String methodName = joinPoint.getSignature().getName();
        // 可以获取方法的参数,这里以第一个参数为例
        Object[] args = joinPoint.getArgs();
        // 打印日志
        System.out.println("Before method: " + methodName + ", with arguments: " + Arrays.toString(args));
    }

    // 在方法执行之后记录日志
    @After("handleMethod()")
    public void logAfter(JoinPoint joinPoint) {
        // 获取被拦截的方法名
        String methodName = joinPoint.getSignature().getName();
        // 打印日志
        System.out.println("After method: " + methodName);
    }
}

7.代码解耦

可以使用注解+AOP 辅助实现

比如:实现一个接口调用时间的统计。

低配版:

public BaseRspsMsg getBusiInfo() {
        BaseRspsMsg baseRspsMsg = new BaseRspsMsg();
        //开始时间
        long startTime = System.currentTimeMillis();
        try {
            //从数据库查
        } catch (Exception e) {
           //异常处理
        }
        
        //结束时间
        long endSendTime = System.currentTimeMillis();
         log.info(" 执行结束耗时:{}S", (endSendTime - startTime) / 1000);
        return baseRspsMsg;
    }

高配版:使用注解+AOP

//定义注解实现时间统计
@Aspect
@Slf4j
@Component
public class TimeAspect {

    //扫描对应标有注解的方法
    @Around(value = "@annotation(com.business.annot.TimeStatistics)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        String methodName = joinPoint.getSignature().getName();
        stopWatch.start(methodName);
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            log.info("======= 调用【" + methodName + "】方法执行耗时 ======= {} s", stopWatch.getTotalTimeSeconds());
        }
    }
}  

在需要统计方法上加一个注解即可轻松实现。

@TimeStatistics
public BaseRspsMsg getBusiInfo() {
        BaseRspsMsg baseRspsMsg = new BaseRspsMsg();
        //开始时间
        long startTime = System.currentTimeMillis();
        try {
            //从数据库查
        } catch (Exception e) {
           //异常处理
        }
        return baseRspsMsg;
}

看,一处定义,随处使用。是不是很清爽?

8.模型抽离

这个主要针对工程设计,从项目全局的角度考量。一般微服务项目设计也是这样的。主要提及:

工程结构:

project
     |___ pro-base    //版本定义
     |___ pro-core    //业务核心数据流
     |___ pro-common  //公共实体,工具类、Feign定义、通用方法等
     |___ pro-dao     //数据层逻辑
     |___ .....       //其它业务服务

9.设计模式

设计模式 的使用也是为了解耦,符合代码设计的 单一职责、开闭原则 。

关于常用的几种设计模式,读者可参阅原来的一篇文章。

工作中常用的几种设计模式

10.接口隔离

接口隔离原则强调的是接口设计应该满足特定的客户端需求,将大型接口拆分成更小的、特定的接口. 和高内聚的思想相辅相成。

比如:一个电子商务平台,该平台需要处理不同类型的支付方式。

假设我们有一个Payment接口,它包含了所有支付方式的通用方法:

public interface Payment {
    void pay(double amount);
    void refund(double amount);
}

这个接口被两个类实现:CreditCardPayment和PayPalPayment。它们都实现了Payment接口,即使PayPalPayment可能不需要refund方法。

public class CreditCardPayment implements Payment {
    public void pay(double amount) {
        // 实现信用卡支付逻辑
    }

    public void refund(double amount) {
        // 实现信用卡退款逻辑
    }
}

public class PayPalPayment implements Payment {
    public void pay(double amount) {
        // 实现PayPal支付逻辑
    }

    // PayPalPayment 实际上不需要实现 refund 方法
    public void refund(double amount) {
        throw new UnsupportedOperationException("Refunds not supported for PayPal");
    }
}

应用接口隔离原则

我们可以将Payment接口拆分成两个更具体的接口:

public interface ICreditCardPayment {
    void pay(double amount);
    void refund(double amount);
}

public interface IPayPalPayment {
    void pay(double amount);
}

然后,我们修改CreditCardPayment和PayPalPayment类,让它们实现相应的接口:

public class CreditCardPayment implements ICreditCardPayment {
    public void pay(double amount) {
        // 实现信用卡支付逻辑
    }

    public void refund(double amount) {
        // 实现信用卡退款逻辑
    }
}

public class PayPalPayment implements IPayPalPayment {
    public void pay(double amount) {
        // 实现PayPal支付逻辑
    }
}

由此可见, 通过应用接口隔离原则,我们减少了类之间的不必要依赖,提高了系统的灵活性和可维护性。每个类只实现了它需要的接口,而不是一个大而全的接口,这使得代码更加清晰和易于理解。

责任编辑:赵宁宁 来源: 码易有道
相关推荐

2012-03-06 16:01:04

项目管理

2024-02-19 14:50:42

编码原则软件开发

2023-09-22 12:04:53

Java代码

2020-08-23 21:07:16

编程PythonJava

2012-05-15 01:38:18

编程编程技巧编程观点

2023-10-31 16:22:31

代码质量软件开发Java

2012-09-28 09:12:39

移动Web

2012-08-02 09:14:13

编程戒律

2016-04-13 11:18:08

jQuery代码片段Web开发

2009-01-15 09:57:00

2011-08-02 21:16:56

查询SQL性能优化

2011-04-14 11:43:47

2021-03-18 09:00:00

微服务架构工具

2022-09-09 16:27:09

微服务架构数据存储

2011-07-27 09:17:20

.NET设计架构

2010-06-22 10:00:31

SQL查询

2011-05-30 15:59:47

编程

2022-02-14 00:16:17

数据安全云安全

2018-08-17 15:33:37

2024-05-13 13:13:13

APISpring程序
点赞
收藏

51CTO技术栈公众号