工程经验 - Java 函数参数

785

一、问题

最近重构后端业务,碰到的一件事是:要将函数作为参数,传递到一个函数里去,函数里的执行流程大概是

  • 函数 f1 的两个参数都是函数 f2 和 f3
  • 在 f1 里调用 f2 和 f3,f1 内的调用过程为:f2 调用后,它的返回值是一个集合,对该集合的每一个元素执行 f3

总共有两个参数,两个参数都是函数,执行的伪代码大概是:

void f1(f2, f3) {
  Param p = new Param();
  valueList = f1.execute.(p);
  valueList.forEach(f3);
}

这个在 C 语言是件很简单的事情,传递「函数指针」 即可,在 C# 中记得用「委托」即可,但在 Java 中还没做过

二、解决方案

后面看了一下,Java 8 提供了四个常用函数式接口,在 java.util.function 下:

  • Consumer(消费型)
  • Supplier(供给型)
  • Predicate(谓词型)
  • Function(功能性)

此时问题中的 f2 使用 Function<T, R> 做,f3 使用 Consumer<T> 来做即可

三、实例

这个实例也是自己 YY 的(公司原因,不大好拿公司业务场景举例 )

3.1 业务场景

假设产品来了需求,要对产品的几个实体做一个展示 show():

  • 只打印产品的 id 为 1 ~ 5 的实体
  • 现在有三个实体要展示:Book、Cup、Pen
  • 三个实体的展示逻辑都不一样

3.2 实现

怎么做?对于 Java Web 工程来说,首先想到的就是在对应的 service 里面去做查询、然后再按照需求对三个实体去写各自展示的业务逻辑

问题是什么?

可以发现它们有一个共有的逻辑:只打印 id 为 1 ~ 5 的实体,那万一产品改了,今天变成 6 ~ 10 ,明天变成 11 - 15 怎么搞?

所以,更好的做法是,把共有的逻辑抽取出来,放到工具类,这样业务只要关注两件事:

  1. 怎么查
  2. 怎么打印

这样就增强了开放性了

对应第一章节的描述,可以这么看:

  • 工具类为 ShowUtil
  • f1 定义为一个展示功能的函数 show()
  • f2 定义不同实体的查询方法 query
  • f3 定义不同的打印方法 print
  • 并且产品规定 show() 只打印 id 为 1 ~ 5 的实体

伪代码为:

public class ShowUtil {
  public static void show(query(), print()) {
    List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5);
    List queryResult = query(ids);
    queryResult.forEach(print());
  }
}

此时业务方使用 ShowUtil.show() ,专注于自己业务的 query() 和 print() 即可

代码实现

例如,三个实体类 Book、Hat、Pen ,都需要展示,以 Book 和 Hat 为例子,实现一下使用工具类传方法参数的做法

定义实体和数据库查询 ORM 接口(这里使用 mybatis-plus)

Book

@Data
public class Book {
    private Integer id;
    private String  name;
}
@Repository
public interface BookMapper extends BaseMapper<Book> {

}

Hat

@Data
public class Hat {
    private Integer id;
    private String  name;
}
@Repository
public interface HatMapper extends BaseMapper<Hat> {
}

Pen

@Data
public class Pen {
    private Integer id;
    private String  name;
}

ShowUtil :

@UtilityClass
public class ShowUtil {
    public static <R> void show(Function<List<Integer>, List<R>> query, Consumer<R> print) {
        List<Integer> param = Arrays.asList(1, 2, 3, 4, 5);
        List<R> records = query.apply(param);
        records.forEach(print);
    }
}

此时,业务方为

@Component
public class Application {
    @Autowired
    BookMapper bookMapper;

    @Autowired
    HatMapper hatMapper;

    public void run() {
        // book 展示的业务
        ShowUtil.show(new Function<List<Integer>, List<Book>>() {
            @Override
            public List<Book> apply(List<Integer> integers) {
                return bookMapper.selectBatchIds(integers);
            }
        }, new Consumer<Book>() {
            @Override
            public void accept(Book book) {
                System.out.println("Book print");
                System.out.println(book.getName());
            }
        });

        // Hat 展示的业务
        ShowUtil.show(new Function<List<Integer>, List<Hat>>() {
            @Override
            public List<Hat> apply(List<Integer> integers) {
                return hatMapper.selectBatchIds(integers);
            }
        }, new Consumer<Hat>() {
            @Override
            public void accept(Hat hat) {
                System.out.println("Hat print");
                System.out.println(hat.getName());
            }
        });
    }
}

匿名函数比较长,可以采用 lambda 做:

@Component
public class Application {
    @Autowired
    BookMapper bookMapper;

    @Autowired
    HatMapper hatMapper;

    public void run() {
        // book 展示的业务
        ShowUtil.show(
                integers -> bookMapper.selectBatchIds(integers),
                book -> {
            System.out.println("Book print");
            System.out.println(book.getName());
        });

        // Hat 展示的业务
        ShowUtil.show(integers -> hatMapper.selectBatchIds(integers), hat -> {
            System.out.println("Hat print");
            System.out.println(hat.getName());
        });
    }
}

四、总结

Function<T, R> 中,R 为响应值,T 为参数,在本实例中,T 是非泛型直接声明的,因此方法声明就使用 R,但用 T 来做也是可以的