掌握Java函数式接口,轻松实现依赖反转

开发 前端
Java函数式接口是多年前引入的,它们对我们开发Java的方式产生了很大的影响。我们可以将它们用作lambda函数,也可以用来反转依赖关系,使我们的代码更加简洁。

你是否考虑过使用Java函数式接口来反转Java项目内的依赖关系?在本文中,我们将探讨如何通过使用三个关键接口——Supplier、Consumer和Function来实现这一目标。

1. Supplier

Supplier接口用于在不需要任何输入参数的情况下提供一个对象,以下是Supplier接口的定义。

public interface Supplier{
   T get();
}

为了更好地理解使用这个接口的必要性,让我们看一看下面的代码。

public class Logger{
   public void log(String message){
      if(isLogEnabled()){
        write(message);
      }
   }
}

// 使用Logger类
public class Controller{
   @Inject Logger logger;

   public void execute(){
      logger.log(generateLogMessage());
   }
}

在上面的代码中,我们有一个Logger类负责在日志被启用时写入日志消息。Controller类通过调用generateLogMessage方法来向Logger类传递消息。到目前为止,一切看起来都很顺利。

然而,试想一下,如果generateLogMessage方法涉及大量处理或消耗大量资源,而日志记录又被禁用了,那么这些有价值的资源就白白浪费了,因为生成的日志消息不会被使用。

解决这个问题的办法是向Logger类传递一个Supplier,它将在需要时返回消息,而Logger类只需在日志被启用时调用该方法即可,代码如下所示。

public class Logger{
   public void log(Supplier messageSupplier){
      if(isLogEnabled()){
        write(messageSupplier.get());
      }
   }
}

// 使用Logger类
public class Controller{
   @Inject Logger logger;

   public void execute(){
      logger.log(() -> generateLogMessage());
   }
}

现在,generateLogMessage方法只会在Supplier的get方法被调用时执行,这样我们就能在日志未启用时节省资源。此外,通过使用Supplier这种解决方案,我们可以灵活地实现复杂的日志记录逻辑,并确保它只会在需要时被调用。

2. Function

通过Function接口,可以定义一个接收参数并产生结果的函数。以下是Function接口的定义(省略了一些默认方法)。

public interface Function{
   R apply(T t);
}

为了开始探索Function接口,让我们来看一个负责计算销售订单中商品价格的类。这个类需要接收输入来计算最终价格,输入包括产品、数量和适用的折扣(0到100之间)等。

public class PriceCalculator{
   public BigDecimal calculatePrice(Product product, 
                                    Integer quantity,
                                    BigDecimal discount){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     return grossPrice.minus(discountAmount);
   }
}

// 使用示例
var result = priceCalculator(product, 10, BigDecimal.value(10));

这个类首先计算总价,然后应用折扣,再从总价中减去折扣金额。现在,让我们考虑一个新的需求:对价格进行货币转换。

一种方法可能是直接将货币转换逻辑添加到这个类中,这可能会带来错误。更稳健的解决方案是引入一个负责处理货币转换的Function参数。

public class PriceCalculator{
   public BigDecimal calculatePrice(
                        Product product, 
                        Integer quantity, 
                        BigDecimal discount, 
                        Function converterFunction){
     var grossPrice = product.getUnitPrice()
                             .multiply(BigDecimal.valueOf(quantity));
     var discountAmount = grossPrice.multiply(discount)
                                    .divide(BigDecimal.valueOf(100));
     var netPrice = grossPrice.minus(discountAmount);
     return converterFunction.apply(netPrice);
   }
}

// 使用示例
var result = priceCalculator(product, 
                             10, 
                             BigDecimal.value(10),
                             netPrice -> netPrice.multiply(CURRENCY_RATE));

增加这个新需求对代码的影响很小,我们成功地反转了依赖关系。PriceCalculator类不再需要处理货币转换;相反,它只是用提供的函数调用净价,并返回结果。这种设计使我们能够在不修改PriceCalculator类的情况下,使用相同的类转换为任何货币。

还有其他一些方法可以满足这个需求,而不需要修改PriceCalculator类。你可以创建另一个类,充当调用PriceCalculator的外观,然后进行货币转换。通常,采用哪种解决方案是由具体项目决定的。

3. Consumer

Consumer接口支持定义一个接收参数、执行特定任务但不返回任何值的函数。以下是Consumer接口的定义(省略了一些默认方法)。

public interface Consumer{
   void accept(T t);
}

为了解Consumer接口的运行示例,我们来看看这个类,它在实体中设置了一些信息,并将其保存到数据库中。

public class EntitySaver{
   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
   }
}

// 使用示例
entitySaver.create(entity);

现在,假设我们需要在创建实体时通知其他类,但我们无法修改create方法的接口。在这种情况下,我们可以使用Consumer接口来实现发布-订阅模式,下面是我们实现该模式的方法。

public class EntitySaver{
   private List> consumerList = new ArrayList<>();

   public void register(Consumer consumer){
      consumerList.add(consumer);
   }

   public void create(Entity entity){
      entity.setCreationDate(new Date());
      database.insert(entity);
      consumerList.forEach(consumer -> consumer.accept(entity));
   }
}

// 使用示例
entitySaver.register(entity -> log.info(entity));
entitySaver.register(entity -> mailerService.notifyUser(entity));
entitySaver.create(entity);

在这个发布-订阅模式的实现中,我们使用了Consumer接口。EntitySaver类现在维护了一个消费者列表,并包含了一个register方法来添加消费者到这个列表中。虽然create方法的接口保持不变,但我们引入了一行代码来“消费”创建的实体,方法是调用已注册的消费者。

4. 结语

Java函数式接口是多年前引入的,它们对我们开发Java的方式产生了很大的影响。我们可以将它们用作lambda函数,也可以用来反转依赖关系,使我们的代码更加简洁。

责任编辑:武晓燕 来源: Java学研大本营
相关推荐

2009-12-10 11:02:44

PHP函数eval()

2020-09-24 10:57:12

编程函数式前端

2024-02-27 08:22:56

2023-05-12 08:02:43

分布式事务应用

2024-03-13 07:49:15

.NET依赖注入DI

2024-04-18 08:39:57

依赖注入控制反转WPF

2024-02-28 08:37:28

Lambda表达式Java函数式接口

2009-12-17 14:36:57

Ruby on Rai

2010-01-13 17:47:59

VB.NET拖放

2024-04-15 07:00:00

Python开发Hatch

2024-05-10 07:19:46

IOC依赖倒置控制反转

2010-01-18 19:36:52

VB.NET调整控件

2023-10-20 08:01:08

2022-04-30 08:50:11

控制反转Spring依赖注入

2019-09-18 18:12:57

前端javascriptvue.js

2010-01-06 17:51:26

Linux关机命令

2022-11-06 21:50:59

Python编程函数定义

2024-07-18 08:00:00

2024-09-12 14:50:08

2023-11-09 08:18:31

Hystrix保护系统资源隔离
点赞
收藏

51CTO技术栈公众号