Post

Java函数式编程实践

函数式编程思想在云服务的兴起(尤其是Faas)下再次兴起,纯函数的无状态特性使得服务的横向扩展能力得到大大提升,主打云原生的Go语言也是以函数为一等公民。

关于何为函数式编程(Functional Programming),可参考 Functional programming WIKI,函数即使对一组将输入映射到输出的表达式,其不改变程序运行状态(Side effect free):

It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.

When a pure function is called with some given arguments, it will always return the same result, and cannot be affected by any mutable state or other side effects.

受到以上趋势的影响,Java在8版本后推出了 Function、Stream相关接口和类,其包名分别为:

  • java.util.stream
  • java.util.function

使用stream和function能够大大提升代码可读性,减少if-else,使得遍历和方法调用更加灵活。

同时,Java 8 引入了lambda表达式这个语法来代替匿名类,使得function的调用更加简洁。

Note: 关于为什么lambda表达式(i.e.匿名类)中使用的局部变量必须是 final or effectively final ,可参考 Variable used in lambda expression should be final or effectively final15.27.2. Lambda Body。简单来说,由于局部变量存在重排序,这样可以防止并发问题,实例变量或类变量就不存在该问题。

DTO转换

在编码过程中经常出现以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserFacade{

    UserRepo userRepo;
    
    public List<UserDTO> getAllUsers(){
    
        List<User> users = userRepo.findAll();
        
        List<UserDTO> userDTOs = new ArrayList<>();
        for (User user: users){
            UserDTO dto = new UserDTO();
            dto.setUsername(user.getUsername());
            dto.setFullName(user.getFirstName()+" "+user.getLastName());
            dto.setActive(user.getDeactiveionDate()==mull);
            userDTOs.add(dto);
        }
        
    }
}

太冗长了,初步尝试把对象复制过程refactor出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserFacade{

    UserRepo userRepo;
    
    public List<UserDTO> getAllUsers(){
    
        return userRepo.findAll().stream().map(this::toDTO).collect(toList());
        
    }
    
    private UserDTO toDTO(User user){
        UserDTO dto = new UserDTO();
        dto.setUsername(user.getUsername());
        dto.setFullName(user.getFirstName()+" "+user.getLastName());
        dto.setActive(user.getDeactiveionDate()==mull);
        return dto;
    }
}

进一步,我们可以把DTO的转换过程提取出来:可以定一个转换类(使用mapstruct或者自定义converter)或者将转换过程写入UserDTO中:

1
2
3
4
5
6
7
8
9
10
class UserFacade{

    UserRepo userRepo;
    
    public List<UserDTO> getAllUsers(){
    
        return userRepo.findAll().stream().map(UserDTO::new).collect(toList());
        
    }
}

但是,如果DTO的转换过程涉及一些依赖的注入,在UserDTO内部进行转换的方式就行不通了,所以使用以下方式注入,并转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserFacade{

    @Autowired
    OtherDependency dependency;

    UserRepo userRepo;
    
    public List<UserDTO> getAllUsers(){
    
        return userRepo.findAll().stream().map(this::toDTO).collect(toList());
        
    }
    
    private UserDTO toDTO(User user){
        UserDTO dto = new UserDTO();
        dto.setxxx(dependency.xxx(user.getxxx))
        dto.setUsername(user.getUsername());
        dto.setFullName(user.getFirstName()+" "+user.getLastName());
        dto.setActive(user.getDeactiveionDate()==mull);
        return dto;
    }
}

同样,可以提取转换过程为一个转换类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class UserFacade{

    UserRepo userRepo;
    
    @Autowired
    UserConverter converter;
    
    public List<UserDTO> getAllUsers(){
    
        return userRepo.findAll().stream().map(converter::toDTO).collect(toList());
        
    }
    
   
}

@Component
class UserConverter{

     @Autowired
    OtherDependency dependency;

     private UserDTO toDTO(User user){
        UserDTO dto = new UserDTO();
        dto.setxxx(dependency.xxx(user.getxxx))
        dto.setUsername(user.getUsername());
        dto.setFullName(user.getFirstName()+" "+user.getLastName());
        dto.setActive(user.getDeactiveionDate()==mull);
        return dto;
    }

}

Optional优雅处理Null

1
2
3
4
5
6
7
8
9
10
11
12
13
class DiscountService{

    public String getDiscountLine(Custom custom){
        return "discount%" + getDiscountPercentage(customer.getMemberCard());
    }
    
    public Integer getDiscountPercentage(MemberCard card){
        if(card.getFidelityPoints()>=100){
            return 5;
        }
        return null;
    }
}

以上代码很容易出现NPE,可以使用Optional处理null:

1
2
3
4
5
6
7
8
9
10
11
12
13
class DiscountService{

    public String getDiscountLine(Custom custom){
       return getDiscountPercentage(customer.getMemberCard().map(d -> "discount%" + d).orElse(""); 
    }
    
    public Optional<Integer> getDiscountPercentage(MemberCard card){
        if(card.getFidelityPoints()>=100){
            return of(5);
        }
        return empty();
    }
}

使用Optional可以优雅地处理null,但是其中card.getFidelityPoints()仍然可能NPE,尝试以下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class DiscountService{

    public String getDiscountLine(Custom custom){
       return getDiscountPercentage(customer.getMemberCard().map(d -> "discount%" + d).orElse(""); 
    }
    
    public Optional<Integer> getDiscountPercentage(MemberCard card){
        // 加一行判断
        if(card==null){
            return empty();
        }
        if(card.getFidelityPoints()>=100){
            return of(5);
        }
        return empty();
    }
}

看到null,又可以使用Optional:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将Custom.getMemberCard()设置为返回Optional
class DiscountService{

    public String getDiscountLine(Custom custom){
        return customer.getMemberCard() // 此行返回Optional<MemberCard>
        // 这里flatMap ,否则d是一个Optional
        // 这是Opitonal.flatMap而不是Stream的
        .flatMap(card->getDiscountPercentage(card))
        .map(d->"Discount%:"+d)
        .orElse("");
    }
    
    public Optional<Integer> getDiscountPercentage(MemberCard card){
        if(card.getFidelityPoints()>=100){
            return of(5);
        }
        return empty();
    }
}

Consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// spring data
interface OrderRepo extends JpaRepository<Order,Long>{
    Stream<Order> findByActiveTrue();
}

class OrderExporter{
    OrderRepo repo;
    
    public File exportFile(String fileName){
        File file = new File(fileName);
        
        try(Writer writer = new FileWriter(file)){
            writer.write("ID:date\n");
            repo.findByActiveTrue()
                .map(o->o.getId()+";"+o.getCreateDate())
                .forEach(Uncheck.consumer(writer::write));
            return file;
        } catch (...){...}
        
    }

}

提取写入文件的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// spring data
interface OrderRepo extends JpaRepository<Order,Long>{
    Stream<Order> findByActiveTrue();
}

class OrderExporter{
    OrderRepo repo;
    
    public File exportFile(String fileName){
        File file = new File(fileName);
        try(Writer writer = new FileWriter(file)){
          writeContent(wirter); 
        } catch (...){...}
        
    }
    
    protected void writeContent(Writer writer) throw Exception{
         writer.write("ID:date\n");
            repo.findByActiveTrue()
                .map(o->o.getId()+";"+o.getCreateDate())
                .forEach(Uncheck.consumer(writer::write));
            return file;
    }

}

writeContent的方式可能有很多,通过extends+protected方法来实现多态:

1
2
3
4
5
6
7
// extends is evil !!!
class HackingTime extends OrderExporter{
    
    protected void wirteContent(Writer writer) throws Exception{
        // ...
    }
}

不要这么做,即使使用abstract方法,组合大于继承。我们看到writeContent是一个返回void地函数,是一个Consumer!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface OrderRepo extends JpaRepository<Order,Long>{
    Stream<Order> findByActiveTrue();
}

class Exporter{

    public static void main(String... args){
        var order = new OrderExporter()
    
        new Exporter().exportFile("filename",order::writeContent);
    }
    
    
    public File exportFile(String fileName,Consumer<Writer> contentWriter){
        File file = new File(fileName);
        try(Writer writer = new FileWriter(file)){
          // ...
          contentWriter.accept(writer); 
        } catch (...){...}
        
    }
}
class OrderExporter{
    OrderRepo repo;
    protected  void writeContent(Writer writer){
        // ...
    }
}
This post is licensed under CC BY 4.0 by the author.