避免魔法值和多层if的关键:编程范式和设计模式

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

提示:避免魔法值和多层if的关键:编程范式和设计模式:

设计原则:

开放封闭原则(OCP):通过扩展枚举实现新功能,而非修改现有代码

单一职责原则(SRP):将区域ID与处理逻辑解耦,各自独立变化

  1. 枚举类 封装区域ID与处理逻辑的映射关系
  2. BiConsumer 函数式接口实现动态方法调用
  3. Stream API 函数式遍历替代过程式循环

提示:以下是本篇文章正文内容,下面案例可供参考

一、案例分析

java 复制代码
    private void saveAreaStatisticsDaily(List<ObjectA> result, LocalDateTime CCCTime) {
        ObjectB objectB = new ObjectB();
        result.stream().forEach(po -> {
            if(po.getAreaId().equals("1669")){
                objectB.setRightOfCCCrOAD(po.getFlow());
            }else if (po.getAreaId().equals("1670")){
                objectB.setLeftOfCCCrOAD(po.getFlow());
            }else if (po.getAreaId().equals("1671")){
                objectB.setUndergroundRingrOAD(po.getFlow());
            }else if (po.getAreaId().equals("1672")){
                objectB.setXtUnderpassrOAD(po.getFlow());
            }else if (po.getAreaId().equals("1673")){
                objectB.setJbxRightOfUnderpassrOAD(po.getFlow());
            }else if (po.getAreaId().equals("1674")){
                objectB.setJbxLeftOfUnderpassrOAD(po.getFlow());
            }
        });
        objectB.setCCCTime(CCCTime);
        objectB.setFlow(result.stream().mapToInt(ObjectA::getFlow).sum());
        dailyStatisticsService.save(objectB);
    }

问题类型 具体表现

  1. 魔法值 区域ID(如"1669")直接硬编码在业务逻辑中
  2. 多层条件判断 大量if-else分支导致代码臃肿
  3. 违反OCP原则 新增区域需修改原有方法,影响稳定性
  4. 可测试性差 条件分支多,单元测试需覆盖所有分支,维护成本高

二、技术手段

函数式接口

函数式编程模式

函数式编程模式是一种设计模式,它将程序分解为一组函数,每个函数都是一个独立的计算。这种模式可以帮助程序员更好地组织代码,提高代码的可读性和可维护性。

java 复制代码
enum AreaIdMapping {
        objectB_1669("1669", (po, value) -> po.setRightOfCrossobjectB(value)),
        objectB_1670("1670", (po, value) -> po.setLeftOfCrossobjectB(value)),
        objectB_1671("1671", ObjectB::setUndergroundRingobjectB),
        objectB_1672("1672", ObjectB::setXtUnderpassobjectB),
        objectB_1673("1673", ObjectB::setJbxRightOfUnderpassobjectB),
        objectB_1674("1674", ObjectB::setJbxLeftOfUnderpassobjectB);

        private final String areaId;
        private final BiConsumer<ObjectB, Integer> setter;

        AreaIdMapping(String areaId, BiConsumer<ObjectB, Integer> setter) {
            this.areaId = areaId;
            this.setter = setter;
        }

        public void apply(ObjectB po, int flow) {
            setter.accept(po, flow);
        }

        private static final Map<String, AreaIdMapping> BY_AREA_ID = Arrays.stream(values())
                .collect(Collectors.toMap(AreaIdMapping::getAreaId, Function.identity()));

        public static Optional<AreaIdMapping> fromAreaId(String areaId) {
            return Optional.ofNullable(BY_AREA_ID.get(areaId));
        }

        public String getAreaId() {
            return areaId;
        }
    }

BiConsumer<T, U> 是 Java 中的一个函数式接口(Functional Interface)。函数式接口指的是仅有一个抽象方法的接口,它们可以用于支持 Lambda 表达式或方法引用。BiConsumer<T, U> 接口位于 java.util.function 包中,它代表了一个接受两个输入参数执行操作且没有返回结果的操作。

java 复制代码
private final BiConsumer<ObjectB, Integer> setter;

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
}

BiConsumer<T, U> 接口包含一个名为 accept 的抽象方法:

此方法接收两个参数,第一个参数的类型为 T,第二个参数的类型为 U,不返回任何结果(即返回类型为 void)

作用:接收两个参数(ObjectB实例和流量值),执行赋值操作

优势:将方法调用抽象为可传递的行为参数

例子: 打印出两个数值的和。你可以使用 BiConsumer 来实现这个功能:

java 复制代码
BiConsumer<Integer, Integer> sumPrinter = (a, b) -> System.out.println("Sum: " + (a + b));
sumPrinter.accept(5, 10); // 输出: Sum: 15

开放封闭原则:软件实体(类、模块等)应该是可扩展的,但不可修改的。

实际的应用场景,比如使用Consumer处理集合元素,用Predicate进行过滤,用Function进行数据转换,这样用户能更好地将理论应用到实际项目中

函数式编程核心接口

  1. Function<T, R> 转化器
    作用:接收一个输入值(T),返回处理后的结果(R)
    常见场景:数据转换、链式处理
java 复制代码
// 字符串转整型
Function<String, Integer> strToInt = s -> Integer.parseInt(s);
int num = strToInt.apply("123"); // 123

// 组合函数:先平方再转字符串
Function<Integer, Integer> square = x -> x * x;
Function<Integer, String> toString = x -> "Result: " + x;
Function<Integer, String> pipeline = square.andThen(toString);
System.out.println(pipeline.apply(3)); // "Result: 9"
  1. Consumer(消费者)
    作用:接收输入值(T),无返回值(void)
    常见场景:遍历集合、日志打印、资源处理
java 复制代码
// 打印集合元素
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println("Name: " + name));

// 组合消费者
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> save = s -> repository.save(s);
Consumer<String> pipeline = log.andThen(save);
pipeline.accept("Data");
  1. Supplier(提供者)
    作用:无输入参数,返回一个值(T)

常见场景:延迟计算、对象工厂、配置获取

java 复制代码
// 生成随机ID
Supplier<String> idGenerator = () -> UUID.randomUUID().toString();
String id = idGenerator.get(); // "550e8400-e29b-41d4-a716-446655440000"

// 懒加载配置
Supplier<Config> configLoader = () -> loadConfigFromDB();
Config config = configLoader.get();
  1. Predicate(断言)
    作用:接收输入值(T),返回布尔判断结果

常见场景:数据过滤、条件验证

java 复制代码
// 过滤偶数
Predicate<Integer> isEven = n -> n % 2 == 0;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.stream().filter(isEven).forEach(System.out::println); // 2,4

// 组合断言
Predicate<String> validLength = s -> s.length() >= 6;
Predicate<String> hasDigit = s -> s.matches(".*\\d.*");
Predicate<String> strongPassword = validLength.and(hasDigit);
boolean isValid = strongPassword.test("pass123"); // true

进阶接口与应用

  1. BiFunction<T, U, R>(双参数转换)
java 复制代码
BiFunction<Integer, Integer, Double> power = (base, exp) -> Math.pow(base, exp);
Double result = power.apply(2, 3); // 8.0
  1. UnaryOperator(一元操作)
    本质:Function<T, T> 的特殊形式
java 复制代码
UnaryOperator<String> trim = s -> s.trim();
String clean = trim.apply("  hello  "); // "hello"
  1. 方法引用(语法糖)

四种形式:

java 复制代码
// 静态方法
Function<String, Integer> parser = Integer::parseInt;

// 实例方法
Consumer<String> printer = System.out::println;

// 对象方法
String str = "example";
Supplier<Integer> lengthSupplier = str::length;

// 构造方法
Supplier<List<String>> listFactory = ArrayList::new;

实战应用场景

  1. 集合处理(Stream API)
java 复制代码
List<Product> products = productRepository.findAll();

// 过滤 + 转换
List<String> expensiveNames = products.stream()
    .filter(p -> p.getPrice() > 100)
    .map(Product::getName)
    .toList();
  1. 策略模式
java 复制代码
Map<String, Function<Order, BigDecimal>> discountStrategies = new HashMap<>();
discountStrategies.put("VIP", order -> order.getTotal().multiply(0.8));
discountStrategies.put("NEW_USER", order -> order.getTotal().subtract(50));

BigDecimal finalPrice = discountStrategies.get(userType).apply(order);
  1. 回调机制
java 复制代码
public void processFile(String path, Consumer<String> lineProcessor) {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        String line;
        while ((line = reader.readLine()) != null) {
            lineProcessor.accept(line);
        }
    }
}

// 使用
processFile("data.txt", line -> {
    if (line.contains("ERROR")) {
        alertService.notify(line);
    }
});
  1. 链式校验
java 复制代码
Predicate<User> nameValid = u -> u.getName().length() >= 2;
Predicate<User> emailValid = u -> u.getEmail().contains("@");
Predicate<User> ageValid = u -> u.getAge() >= 18;

Predicate<User> fullCheck = nameValid.and(emailValid).and(ageValid);
boolean isValid = fullCheck.test(user);

最佳实践与避坑指南

  1. 保持简洁性

避免在Lambda中编写超过3行的逻辑,复杂逻辑应封装为方法

java 复制代码
// 反例
list.forEach(x -> {
    // 10行复杂处理...
});

// 正例
list.forEach(this::processItem);
  1. 避免状态修改

函数式代码应无副作用,不修改外部变量

java 复制代码
// 危险操作
int[] counter = {0};
list.forEach(x -> counter[0]++);

// 安全替代
long count = list.stream().count();
  1. 优先使用已有接口

不需要自定义函数式接口时,尽量使用 java.util.function 中的标准接口

  1. 异常处理

Lambda中处理异常需显式捕获

java 复制代码
list.forEach(s -> {
    try {
        process(s);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
});

在枚举中

BiConsumer<CheckpointAreaDayStatisticsPO, Integer> 被用来存储不同区域对应的设置方法引用。这允许 AreaIdMapping 枚举中的每个实例都携带一个特定的方法,该方法可以应用于 CheckpointAreaDayStatisticsPO 对象,并传递一个整型参数。这种方法避免了编写大量的 if-else 或 switch-case 语句来决定应该调用哪个方法,使得代码更加简洁、清晰和易于维护。例如:

java 复制代码
enum AreaIdMapping {
    TUNNEL_1669("1669", CheckpointAreaDayStatisticsPO::setRightOfCrossTunnel),
    // 其他枚举常量...

    private final String areaId;
    private final BiConsumer<CheckpointAreaDayStatisticsPO, Integer> setter;

    AreaIdMapping(String areaId, BiConsumer<CheckpointAreaDayStatisticsPO, Integer> setter) {
        this.areaId = areaId;
        this.setter = setter;
    }

    public void apply(CheckpointAreaDayStatisticsPO po, int flow) {
        setter.accept(po, flow);
    }
    
    // ...其他方法...
}

枚举类(Enum)在Java中是一个非常强大和灵活的工具,适用于多种不同的场景。它们可以用于定义一组固定的常量、实现简单的状态机、管理配置选项等。以下是几种常见的应用方式及示例:

最常见的用途之一是定义一组固定的常量值。例如,表示一周中的天数:

java 复制代码
public enum DayOfWeek {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

枚举还可以携带属性和方法,使得每个枚举实例能够包含额外的信息。例如,颜色及其对应的RGB值:

java 复制代码
public enum Color {
    RED(255, 0, 0), GREEN(0, 255, 0), BLUE(0, 0, 255);

    private final int r, g, b;

    Color(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public String getRGB() {
        return "(" + r + ", " + g + ", " + b + ")";
    }
}

实现接口的枚举

枚举可以实现接口,允许为不同枚举值提供具体的行为实现。例如,支付方式的不同处理逻辑:

java 复制代码
public interface PaymentMethodProcessor {
    void processPayment(double amount);
}

public enum PaymentMethod implements PaymentMethodProcessor {
    CREDIT_CARD {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing credit card payment of $" + amount);
        }
    },
    PAYPAL {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing PayPal payment of $" + amount);
        }
    };
}

状态模式

枚举非常适合用来实现简单状态机。例如,订单的状态变化:

java 复制代码
public enum OrderStatus {
    PENDING, SHIPPED, DELIVERED, CANCELLED;

    public boolean canTransitionTo(OrderStatus newState) {
        switch (this) {
            case PENDING:
                return newState == SHIPPED || newState == CANCELLED;
            case SHIPPED:
                return newState == DELIVERED;
            case DELIVERED:
            case CANCELLED:
                return false;
            default:
                throw new IllegalArgumentException();
        }
    }
}

枚举与工厂模式结合

枚举可以作为工厂模式的一部分,用于创建对象。例如,根据不同的数据库类型创建相应的连接对象:

java 复制代码
public enum DatabaseType {
    MYSQL, POSTGRESQL;

    public Connection createConnection(String url, String username, String password) throws SQLException {
        switch (this) {
            case MYSQL:
                // 返回MySQL数据库连接
                return DriverManager.getConnection(url, username, password);
            case POSTGRESQL:
                // 返回PostgreSQL数据库连接
                return DriverManager.getConnection(url, username, password);
            default:
                throw new UnsupportedOperationException("Unsupported database type");
        }
    }
}

通过这些例子,可以看到枚举类不仅可以用来定义一组固定的常量,还可以携带数据、实现接口、作为状态机的一部分,甚至是作为工厂模式的一部分来创建对象。这使得枚举成为Java编程中一个非常有用且多功能的工具。

三、优化后完整代码

优化后代码

java 复制代码
  private void saveAreaStatisticsDaily(List<ObjectA> result, LocalDateTime crossTime) {
        ObjectB objectB = new ObjectB();

        // 使用forEach遍历每个ObjectA对象,并根据areaId设置相应的流量
        result.forEach(po ->
                AreaIdMapping.fromAreaId(po.getAreaId())
                        .ifPresent(mapping -> mapping.apply(objectB, po.getFlow()))
        );

        // 设置crossTime和总流量
        objectB.setCrossTime(crossTime);
        objectB.setFlow(result.stream().mapToInt(ObjectA::getFlow).sum());

        dailyStatisticsService.save(objectB);
    }

enum AreaIdMapping {
        objectB_1669("1669", (po, value) -> po.setRightOfCrossobjectB(value)),
        objectB_1670("1670", (po, value) -> po.setLeftOfCrossobjectB(value)),
        objectB_1671("1671", ObjectB::setUndergroundRingobjectB),
        objectB_1672("1672", ObjectB::setXtUnderpassobjectB),
        objectB_1673("1673", ObjectB::setJbxRightOfUnderpassobjectB),
        objectB_1674("1674", ObjectB::setJbxLeftOfUnderpassobjectB);

        private final String areaId;
        private final BiConsumer<ObjectB, Integer> setter;

        AreaIdMapping(String areaId, BiConsumer<ObjectB, Integer> setter) {
            this.areaId = areaId;
            this.setter = setter;
        }

        public void apply(ObjectB po, int flow) {
            setter.accept(po, flow);
        }

        private static final Map<String, AreaIdMapping> BY_AREA_ID = Arrays.stream(values())
                .collect(Collectors.toMap(AreaIdMapping::getAreaId, Function.identity()));

        public static Optional<AreaIdMapping> fromAreaId(String areaId) {
            return Optional.ofNullable(BY_AREA_ID.get(areaId));
        }

        public String getAreaId() {
            return areaId;
        }
    }

总结

通过函数式接口与枚举的结合,实现了 行为参数化,使代码更符合现代编程范式。这种模式不仅适用于本例,还可广泛应用于状态机、策略选择等场景,是提升代码质量的利器。

相关推荐
嗨起飞了15 分钟前
Maven快速入门指南
java·maven
A boy CDEF girl31 分钟前
【JavaEE】线程池
java·java-ee
Joeysoda31 分钟前
JavaEE进阶(2) Spring Web MVC: Session 和 Cookie
java·前端·网络·spring·java-ee
Y雨何时停T1 小时前
深入理解 Java 虚拟机之垃圾收集
java·开发语言
动亦定1 小时前
物联网设备接入系统后如何查看硬件实时数据?
java·物联网
码农CV1 小时前
Java基础面试题全集
java·面试
B站计算机毕业设计超人2 小时前
计算机毕业设计SpringBoot+Vue.js民族婚纱预定系统(源码+文档+PPT+讲解)
java·vue.js·spring boot·后端·毕业设计·课程设计·毕设
DemonAvenger2 小时前
深入Go并发编程:Goroutine性能调优与实战技巧全解析
设计模式·架构·go
镜花水月linyi2 小时前
记一次SpringBoot传给前端Long类型精度丢失
java·后端
技术咖啡馆C2 小时前
二、IDE集成AI助手豆包MarsCode保姆级教学(使用篇)
java·ide·ai编程·idea-plugin·marscode