【Java 8实战笔记】通过行为参数化传递代码

通过行为参数化传递代码

行为参数化是可以帮助你处理频繁变更的需求的一种软件开发模式。它意味着拿出一个代码块,将它准备好却不去执行它,这个代码块可以被程序的其他部分调用。
将代码块作为参数传递给另一个方法,稍后再去执行它。这样这个方法就基于那块代码被参数化了。打个比方,可能会这样处理一个集合:

  • 可以对列表中的每个元素做 “某件事”
  • 可以再列表处理完后做 “另一件事”
  • 遇到错误时可以做 “另外一件事”

这样一个方法便可以接受不同的新行为作为参数去执行。

行为参数化

举个栗子:假设要求你对苹果的不同属性做筛选,比如大小、形状、产地、重量等,写好多个重复的filter方法或者一个巨大的非常复杂的方法都不是好办法。为此需要一种更好的方法,来把苹果的选择标准告诉filterApples方法。更高层次的抽象这个问题,对选择标准进行建模:苹果需要根据Apple的某些属性来返回一个boolean值,这需要一个返回boolean值的函数,我们把它称为谓词

现在我们定义一个接口来对选择标准建模:

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

现在就可以用ApplePredicate的多个实现代表不同的选择标准了:

    static class AppleWeightPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return apple.getWeight() > 150; 
        }
    }
    static class AppleColorPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return "green".equals(apple.getColor());
        }
    }

    static class AppleRedAndHeavyPredicate implements ApplePredicate{
        public boolean test(Apple apple){
            return "red".equals(apple.getColor()) 
                    && apple.getWeight() > 150; 
        }
    }
选择苹果的不同策略

这些标准可以看做filter方法的不同行为。上面做的这些和“策略设计模式”相关,它让你定义一族算法,把它们封装起来,然后运行的时候选择一个算法,这里的算法族就是applePredicate,不同的策略就AppleHeavyWeightPredicateAppleGreenColorPredicate

下面的问题是,要怎么利用applePredicate的不同实现。需要让filterApples方法接受applePredicate对象,对Apple做条件测试。这就是行为参数化:让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。

为此需要给filterApples方法添加一个参数,让它能接受ApplePredicate对象。这样在软件工程上带来的好处就是:将filterApples方法迭代集合的逻辑与你要应用到集合中每个也元素的行为(这里是谓词)区分开了。

由此便可以修改ApplePredicate方法为:

    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);
            }
        }
        return result;
    }

现在便可以创建不同的ApplePredicate对象,并将它们传递给filterApples方法。filterApples方法的行为取决于你通过ApplePredicate对象传递的代码。实现了行为参数化

public class AppleRedAndHeavyPredicate implements ApplePredicate{
    public boolean test(Apple apple){
        return "red".equals(apple.getColor())
            && apple.getWeight() > 150;
    }
}

List<Apple> redAndHeavyApples =
    filterApples(inventory, new AppleRedAndHeavyPredicate());
对付啰嗦

虽然已经可以把行为抽象出来让代码适应需求的变化了,但是这个过程很啰嗦,因为当要把新的行为传递给方法的时候,可能需要声明很多实现接口的类来实例化好几个只要实例化一次的类(划重点,这边说的是对于只要实例化一次的类)。这样又啰嗦又费时间。

对于这个问题也有对应的解决办法,Java有一个机制称为匿名类,它可以让你同时声明和实例化一个类,使代码更为简洁。

匿名类

匿名类和Java局部类差不多,但匿名类没有名字。它允许你同时声明并实例化一个类(随用随建)。

用匿名类实现的ApplePredicate对象:

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

但是这也并不完全令人满意。第一,它代码占用很多行。第二,它用起来容易让人费解。即使匿名类处理在某种程度上改善了为一个接口声明好几个实体类的啰嗦问题,让仍不能让人满意。更好的方式是通过Lambda表达式让代码更易读。

使用Lambda表达式

上面的代码在Java8里可以用Lambda表达式重写为下面的样子:

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

这代码看上去比先前干净很多。因为它看起来更像问题陈述本身了。

将List类型抽象化

目前的filterApples方法还只适用于Apple。还可以将List类型抽象化,从而超越眼前要处理的问题:

public interface Predicate<T>{
    boolean test(T t);
}

public static <T> List<T> filter(List<T> inventory, ApplePredicate<T> p){
    List<T> result = new ArrayList<>();
    for(T e : list ){
        if(p.test(e)){
            result.add(e);
        }
    }
    return result;
} 

现在可以把filter方法用在其他列表上了。

真实的例子

下面将通过两个例子来巩固传递代码的思想。

用Comparator来排序

想要根据不同的属性使用一种方法来表示和使用不同的排序行为来轻松地适应变化的需求。
在Java 8中,List自带了一个sort方法(也可以使用Collections.sort)。sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:

public interface Comparator<T>{
    public int compare(T o1,T o2);
}

因此可以随时创建Comparator的实现,用sort方法表现出不同的行为。

按照重量升序对库存排序:

inventory.sort(new Comparator<Apple>(){
    public int compare(Apple a1,Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

如果用Lambda表达式,它看起来会是这样:

inventory.sort(
    (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

(这段代码并没有告诉你如何同时遍历列表中的两个元素,不要纠结遍历问题,下一章会详细的讲解如何编写和使用Lambda表达式。)

用Runnable执行代码块

线程就像是轻量级的进程:它们自己执行一个代码块。多个线程可能会运行不同的代码。为此需要一种方式来代表要执行的一段代码。在Java里,可以使用Runnable接口表示一个要执行的代码块:

public interface Runnabke{
    public void run();
}

可以像下面这样使用这个接口创建执行不同行为的线程:

Thread t = new Thread(new Runnable()){
    public void run(){
        System.out.println("Hello world");
    }
});

用Lambda表达式的话,看起来会是这样:

Thread t =new Thread(() -> System.out.println("Hello world"));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容