避免魔法值和多层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;
        }
    }

总结

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

相关推荐
Yyyyyy~3 分钟前
【java】数组的定义和使用
java
麻辣布丁4 分钟前
java集合篇面试模拟
java·开发语言·面试
niucloud-admin6 分钟前
JAVA V6 多商户商城 开发文档——插件开发规范
java·开发语言
嘟哩DuliDuli19 分钟前
创作 Agent 的任务状态该如何保存
android·java·javascript
善良勤劳勇敢而又聪明的老杨30 分钟前
【AI编程系列】MCP 常用设计模式解读
设计模式·ai编程
wdfk_prog31 分钟前
lely-canopen-rtt:为什么推荐在 RT-Thread 上使用 Lely CANopen 构建主站
java·运维·缓存·docker·容器
Token掘金室36 分钟前
JSON模式结构化输出报错
java·服务器·json
秋饼42 分钟前
Spring AI 2.0 接入 DeepSeek V4.1 Flash 生产级实战
java·ai·技术分享·后端开发
IT枫斗者枫哥42 分钟前
Java 分批导出仍然 OOM?用 32 MiB 堆复现三种 CSV 写法
java
shehuiyuelaiyuehao1 小时前
算法43,外观数列,模拟算法+双指针
java·算法