美文网首页
Java重新出发--Java学习笔记(十)--函数化编程

Java重新出发--Java学习笔记(十)--函数化编程

作者: 親愛的破小孩 | 来源:发表于2019-08-06 20:01 被阅读0次

根据原作的推荐,这里虽然没有看过这本书,不过贴出来以便能够日后学习的同时也推荐给更多的人

《Java8实战》

这里简单介绍一点Java8这些特性

(1)用行为参数化把代码转递进去

java8中增加了通过API来传递代码的能力,如果你想要写两个只有几行代码不同的方法,那现在只需要把那部分不同的代码作为参数传递进去就可以了。
但这样想来实在也不好理解,还是要通过具体的例子来进行理解。Java8中这样做起来非常的清晰和简介(不仅限于匿名内部类)
我们现在就开始来举这个例子:

比如现在我们接了一个项目是要过滤苹果的相关属性的。目前一期的需求是要过滤出颜色是绿色的苹果,但到了二期又提出了要过滤重量超过150g的苹果。再往后,可能又会提新的需求,既要绿色也要超过150g.看到这里,你可以暂停先不要往下看,思考一下,如果是你,你会如何选择方法来实现呢。
(在学习之前,我的方法是写一个方法,传进苹果对象, 对其进行颜色和大小的判断。但是现在想来这个方法真的非常局限,一个方法用来判断某些属性是不是符合一个值也许可以,但是如果要对很多个属性做判断,或者判断多种值是不是就要写n个方法了?我一直以来的这种思维确实真的非常不适合扩展代码)

理想状态下,你应该把你的工作量降到最小,此外这种功能实现起来要很简单,而且易于长期维护。
下面我们将完成一个思考的过程,从笨拙中体会思考带来的意义,也体会新特性的好处。

第一次尝试:

    //筛选绿苹果
    public static List<Apple> filterGreen(List<Apple> inventory){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if("green".equals(apple.getColor())){
                result.add(apple);
            }
        }
        return result;
    }

这样的代码很好理解,就是过滤绿苹果呗,但是客户现在要求要筛选红苹果了怎么办?简单的方法首先想到就是把函数名和if判断的条件匹配到红色上就好了,然而如过颜色又加了呢?一个良好的原则是在编写类似代码之后,尝试将其抽象化。

第二次尝试:把颜色当成参数
给方法增加参数,将属性灵活化,这样就可以匹配不同的颜色啦。

//根据颜色参数来适应变化
    public static List<Apple> filterAppleByColor(List<Apple> inventory,String color){
        List<Apple> result = new ArrayList<>();
        for(Apple apple:inventory){
            if(apple.getColor().equals(color)){
                result.add(apple);
            }
        }//end for
        return result;
        
    }

现在如果要过滤不同颜色的苹果,只要像下面这样调用就可以了:

List<Apple> greenApples = filterApplesByColor(inventory,"green");
List<Apple> redApples = filterApplesByColor(inventory,"red");

这样想着好像也没有什么难嘛,这个时候问题又来了,客户说我除了颜色之外我还想同时过滤掉轻的苹果,这个时候,你会觉得,那再加进来一个参数好啦,于是代码又变成了这样:

    //根据多种多样的颜色和多种多样的重量来选择
    public static List<Apple> filterApplesByColor(List<Apple> inventory,int weight){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if(apple.getWeight() > weight){
                result.add(apple);
            }
        }  // end for
        return result;
    }

这个方法好像看起来好了很多,但是我们注意到,每次调用这个方法的时候,我们都把库存遍历了一遍,再对每个苹果应用筛选条件。这有点儿令人失望了,因为它违反了DRY工程原则(DON'T Repeat Yourself)你可以将颜色和重量结合为一个方法,成为filter,不过就算是这样,你还是需要一种方式来区分某次调用是要筛选哪个属性,你可以加上一个标志位来区分对颜色或重量的查询。先在这里奉劝不要这么做,后面会展开说为什么。
先写一个将所有属性结合起来的笨拙方法尝试如下:

//利用笨拙的方法结合属性尝试
    public static List<Apple> filterApplesFool(List<Apple> inventory,String color,int weight, boolean flag){
        List<Apple> result = new ArrayList<>();
        for (Apple apple: inventory){
            if((flag && apple.getColor().equals(color)) ||
                    (!flag && apple.getWeight() > weight)){
                    result.add(apple);
            }
        }
        return result;      
    }

你可以这么使用,但是真的很没水准。

List<Apple> greenApples = filterApples(inventory, "green", 0, ture);
List<Apple> heavyApples = filterApples(inventory, "", 150, false);

这个我们回过头来看,真的是很糟糕的一段代码,true和false只看代码你能看出来指代了什么意思嘛?这里可能就需要你手动加上很多注释了,但其实这真的是没有必要的注释。此外更关键的一点是如果甲方又提出了新的属性怎么办呢?形状,产地,尺寸,难道要一个一个加进参数中么?那么在你修改的时候,就可能要给很多代码动刀了。真的非常不利于维护。你就会有非常多不同组合的过滤方法或者一个超级冗杂的大方法。到目前为止,你已经给filterApples方法加上了值(如String、Integer或boolean)的参数。这对于某些确定性问题可能还不错,但如今这种情况下,你需要一种更好的方式,来把苹果的选择标准告诉你的filterApples方法。

这里行为参数化就闪亮登场了。让我们后退一步来看更高层次的抽象。一种可能的方案是对你的选择标准进行建模:你考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值,我们把它称为谓词(即一个返回boolean值的函数)

第四次尝试:根据抽象条件筛选
让我们先来定义一个接口对选择标准建模:

public interface ApplePredicate{
    boolean test(Apple apple);
}

(或者做一个模糊搜索的功能)

    public interface SearchPredicate{
        List test(String name);
    }

现在你可以使用ApplePredicate的多个实现代表不同的选择标准了。

//仅仅选出绿苹果
public class AppleColorPredicate implements ApplePredicate{
    @Override
    public boolean test(Apple apple) {
        // TODO Auto-generated method stub
        return "green".equals(apple.getColor());
    }
}
//仅仅选出重的苹果
public class AppleHeavyWeightPredicate implements ApplePredicate{

    @Override
    public boolean test(Apple apple) {
        // TODO Auto-generated method stub
        return apple.getWeight()>150;
    }
}

你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。
在这里算法簇就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicateAppleGreenColorPredicate。但是,该怎么利用ApplePredicate的不同实现呢?你需要filterApples方法接受ApplePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,完成不同的行为。

利用ApplePredicate改过之后,filter方法看起来就是这样的:

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
    List<Apple> result = new ArrayList<>();
    for(Apple apple:inventory){
        if(p.test(apple)){
            result.add(apple);
        }  // end if
    }    // end for
    return result;
}

这里值得暂停下来小小地庆祝一下。这段代码比我们第一次尝试的时候灵活多了,读起来、用起来也更容易!现在你可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。免费的灵活性!比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicacte对象就可以了,你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了。

这时我们已经做了一件较为灵活的事情了,filterApples方法的行为取决于你通过ApplePredicate对象传递的代码,换句话说,你已经把filterApples方法的行为参数化了!

第五次尝试:使用匿名内部类
上面的方式已经很不错了,还有什么问题呢?
我们都知道,人们不愿意用那些很麻烦的功能或者概念,目前,当把新的行为传递给filterApples方法的时候,你不得不声明好几个实现ApplePredicate接口的类,然后实例化好几个只会用到一次的ApplePredicate对象。下面这段程序总结了你目前看到的一切,这真的很罗嗦并且浪费时间。

public static void main(String...args) {
        // TODO Auto-generated method stub
        List<Apple> inventory = Arrays.asList(new Apple("green",80),
                                              new Apple("green",155),
                                              new Apple("red",120));
        List<Apple> heavyApple = 
                filterApples(inventory,new AppleHeavyWeightPredicate());
        List<Apple> greenApple = 
                filterApples(inventory,new AppleColorPredicate());
}

费这么大劲儿,真的没什么必要。能不能做得更好呢?Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类,它可以帮助你进一步改善代码,让它变得更简洁:

List<Apple> redApples = filterApples(inventory, new Applepredicate(){
    public boolean test(Apple apple){
        return "red".equals(apple.getColor());
    }
});

GUI应用程序中经常使用匿名类来创建事件处理器对象(下面的例子使用的是JavaFX API,一种现代的Java UI平台):

button.setOnAction(new EventHandler<ActionEvent>(){
    public void handle(ActionEvent event){
        System.out.println("Woooo a click!");
    }
});

但是匿名类仍然不够好。

第一,它往往很笨重,因为它占用了很多空间,有很多模板代码。
第二,很多程序员觉得它用起来很让人费解
这里插一条经点谜题:

public class Puzzle {
    public final int value = 4;
    public void doIt(){
        int value = 6;
        Runnable r = new Runnable(){
            public final int value = 5;
            public void run(){
                int value = 10;
                System.out.println(this.value);
            }
        };
        r.run();
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Muzzle m = new Muzzle();
        m.doIt();//输出什么?
    }
}

答案是5,因为this指的是包含它的Runnable,而不是外面的类Puzzle。

整体来说,啰嗦就不好。它让人不愿意使用语言的某种功能,因为编写和维护啰嗦的代码需要很长时间,而且代码也不易读。好的代码应该一目了然。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,但它仍然不能让人满意。在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为(例如Predicate中的test方法或者是EventHandler中的handler方法)。在理想的情况下,我们想鼓励程序员使用行为参数化模式,因为正如你在前面看到的,它让代码更能适应需求的变化,但也同样的,啰嗦不可避免。这也正是Java 8的语言设计者引入Lambda表达式的原因——他让传递代码的方式变得更加简洁、干净。

以上的例子如果拿lambda表达式实现会非常的好看

List<Apple> result = 
    filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

但是基于我们还是单独开一章学习一下lambda表达式的前提,我们这里就先把这个问题收尾,
相信在下一章学习完lambda表达式后,这种优化对你来说根本不是问题。

相关文章

网友评论

      本文标题:Java重新出发--Java学习笔记(十)--函数化编程

      本文链接:https://www.haomeiwen.com/subject/qntgdctx.html