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 final 和 15.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){
// ...
}
}