摘要: 在Java重构的过程中,巧妙的运用函数式思想能够便捷地去掉重复。
函数式编程是声明式的。也就是说,她应该指定“什么要做”而非“怎么做”。这种方式使得我们可以工作更高的抽象层次。而传统的过程式以及面向对象的语言,则是命令式的,因而更关注于“怎么做”这个层面。站在面向对象思想的角度来看,函数式编程将函数看成一等公民的思想,使得我们处理的粒度从类变小为函数,从而可以更好地满足系统对重用性和扩展性的支持。也就是说,我们可以从函数的粒度,而非对象的粒度去思考领域问题。
例如,有这样一个场景:我的业务模型是个Person,已经有业务接口能返回给我所有Person的list集合,我需要把根据一些条件返回符合对应关系的Person。
Person.java
public class Person {
private String name;
private String gender;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
首先,根据业务需求,写出第一个方法 通过姓名找出符合的Person
PersonService.java
import java.util.ArrayList;
import java.util.List;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private static List<Person> list = new ArrayList<Person>();
public static List<Person> findByName(String name){
List<Person> people = new ArrayList<Person>();
for (Person p : list){
if(name.equals(p.getName())){
people.add(p);
}
}
return people;
}
}
接下来,第二个方法根据性别找出符合的Person
import java.util.ArrayList;
import java.util.List;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private static List<Person> list = new ArrayList<Person>();
public static List<Person> findByName(String name){
List<Person> people = new ArrayList<Person>();
for (Person p : list){
if(name.equals(p.getName())){
people.add(p);
}
}
return people;
}
public static List<Person> findByGender(String gender){
List<Person> people = new ArrayList<Person>();
for (Person p : list){
if(gender.equals(p.getGender())){
people.add(p);
}
}
return people;
}
}
功能是实现了,但是要是这样交差,估计得挨骂了,这么多重复代码。仔细观察刚添加的两个方法。发现,除了下面两行以外全都相同:
这里的name
和gender
是两个字符串参数,可以给它们用同样的名字,比如str,这样的话:
这下,不同点就是getName()
和getGender()
,是两个方法,而Java是不能把函数当参数的,这样重构起来就麻烦很多。
想到Java中还有一种迂回的方式传递函数,那就是接口。
新建一个接口Criteria
,在这个接口里隐藏if
条件语句里面的实现细节。这样就可以把findByName()
和findByGender
高度抽象为一个方法find()
。
import java.util.ArrayList;
import java.util.List;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private static List<Person> list = new ArrayList<Person>();
public List<Person> findByName(String name) {
return null;
}
public List<Person> findByGender(String gender) {
return null;
}
public List<Person> find(Criteria criteria){
List<Person> people = new ArrayList<Person>();
for (Person p : list){
if(criteria.matches(p)){
people.add(p);
}
}
return people;
}
}
interface Criteria{
boolean matches(Person person);
}
对于具体的findByName()
方法,只需要实现匹配的细节即可:
public List<Person> findByName(String name) {
return find(new Criteria() {
@Override
public boolean matches(Person person) {
return person.getName().equals(name);
}
});
}
经过改装后的PersonService.java大致像下面这样:
import java.util.ArrayList;
import java.util.List;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(new Criteria() {
@Override
public boolean matches(Person person) {
return name.equals(person.getName());
}
});
}
public List<Person> findByGender(String gender) {
return find(new Criteria() {
@Override
public boolean matches(Person person) {
return gender.equals(person.getGender());
}
});
}
public List<Person> find(Criteria criteria){
List<Person> people = new ArrayList<>();
for (Person p : list){
if(criteria.matches(p)){
people.add(p);
}
}
return people;
}
}
interface Criteria{
boolean matches(Person person);
}
看到这,估计有人要发飙了,说好的重构去重复,结果为了一行代码,增加了无数行无关代码,还无故多了一个类(接口Criteria)。原始代码才25行左右,现在将近40行代码。这个findByName()
和findByGender
方法除了return
后面的不同,剩下的完全一样,虽然那些代码都是IDE自动生成的,但是看起来,这次重构没有起到简化代码的目的。甚至,如果你没有用Java8,那上面的代码需要改动才能编译通过。
这种重构的思想其实属于一种设计模式——策略设计模式,它的意图就是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
当然,如果使用过java8,那么应该也听说过Lambda表达式。如果使用的IDE是Intellij的话,那应该庆幸一下,因为它已经识别到了需要改进的地方。
这样,看起来,代码会整洁许多:
import java.util.ArrayList;
import java.util.List;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(p-> name.equals(p.getName()));
}
public List<Person> findByGender(String gender) {
return find(p-> gender.equals(p.getGender()));
}
public List<Person> find(Criteria criteria){
List<Person> people = new ArrayList<>();
for (Person p : list){
if(criteria.matches(p)){
people.add(p);
}
}
return people;
}
}
interface Criteria{
boolean matches(Person person);
}
我定义了一个看起来多余,但是又必须存在的接口Criteria
。如果项目中有很多需要这样重构的代码,不是得写很多类似的接口?当然,JDK的设计者已经想到了这一点,于是就有了一个公用的接口Predicate
。这样,再整理一下代码:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(p-> name.equals(p.getName()));
}
public List<Person> findByGender(String gender) {
return find(p-> gender.equals(p.getGender()));
}
public List<Person> find(Predicate<Person> predicate){
List<Person> people = new ArrayList<>();
for (Person p : list){
if(predicate.test(p)){
people.add(p);
}
}
return people;
}
}
整理后的代码:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(p -> name.equals(p.getName()));
}
public List<Person> findByGender(String gender) {
return find(p -> gender.equals(p.getGender()));
}
public List<Person> find(Predicate<Person> predicate){
return list.stream().filter(p -> predicate.test(p)).collect(Collectors.toList());
}
}
这样看起来就比刚开始复制/粘贴的代码清爽许多。如果第三个业务需求是根据年龄找出符合条件的Person
,你可以这样写:
public List<Person> findByAge(int age){
List<Person> people = new ArrayList<>();
for (Person person : list) {
if(age == person.getAge()){
people.add(person);
}
}
return people;
}
中规中矩,没有问题,但是如果写成下面这样,是不是更容易抓住方法的要点:
public List findByAge(int age){
return find(p -> age == p.getAge());
}
刚开始重构花了很多时间,但是现在要新添加业务方法,我只需要一行代码:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(p -> name.equals(p.getName()));
}
public List<Person> findByGender(String gender) {
return find(p -> gender.equals(p.getGender()));
}
//根据年龄找出符合条件的Person
public List<Person> findByAge(int age){
return find(p -> age == p.getAge());
}
public List<Person> find(Predicate<Person> predicate){
return list.stream().filter(p -> predicate.test(p)).collect(Collectors.toList());
}
}
之后又有一个新的业务需求找出在给定年龄段的Person
,依然只需要一行代码:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PersonService {
//假设这个list是通过已有接口返回的所有Person集合
private List<Person> list = new ArrayList<>();
public List<Person> findByName(String name) {
return find(p -> name.equals(p.getName()));
}
public List<Person> findByGender(String gender) {
return find(p -> gender.equals(p.getGender()));
}
public List<Person> findByAge(int age){
return find(p -> age == p.getAge());
}
//找出在给定年龄段的Person
public List<Person> findByAgeRange(int startAge,int endAge){
return find(p -> startAge <= p.getAge() && endAge >= p.getAge());
}
public List<Person> find(Predicate<Person> predicate){
return list.stream().filter(p -> predicate.test(p)).collect(Collectors.toList());
}
}
Java8中引入的函数式接口,Lambda表达式,以及Stream API给Java增加了更多的可能,在面向对象的语言中增加面向函数的设计思想,使得某些场景下代码的编写异常简洁清晰。