Java 8 函数式编程与面向对象式编程

综述

下面以一个常见的需求为例,分析Java 8的函数式编程与常规的面向对象式编程的不同之处。函数式编程和面向对象式编程最根本的不同之处在于,在面向对象的世界,函数功能不能独立于数据而存在,一个函数功能必须存在于一个包含数据的对象中,服务于特定的数据。也就是说,在面向对象时,对象是编程的最小单元,而一个对象将数据和作用于该数据的函数功能打包成一个整体,数据和函数是不可分割的一部分。此时函数只能为该数据服务,而该数据一般也只能使用定义于其上的函数。而这种函数与数据的结合其实是加深了数据和功能的耦合性。对于编程来说,紧耦合意味着不灵活和低泛化能力。

面向对象的编程中的诸多设计原则如面向接口编程原则,扩展大于继承的原则等都是为了降低对象间的耦合程度而努力。但面向对象编程是有原罪的,它从出生的那一刻起就将数据和函数绑定到一起。这一点决定了面向对象编程不可能做到彻底的解耦。而函数式编程的目的是将解偶进行到底。他要彻底的解耦。他要做的就是将数据和函数分开,函数是函数,数据是数据,没必要非得在一起。在函数式编程的世界,函数和数据是对等的位置,都作为最小的单元而出现。我的数据可以选择A函数来提供服务,也可以选择B函数来提供服务。我的函数不仅可以给C数据提供服务,也可以给D数据提供服务。我可以按我的需求来随意组合数据和函数,打破在面向对象式编程的世界中数据与函数绑定规则,拥抱更自由的世界。

在java上谈论函数式编程对有些人来说是一件可笑的事情,因为java从本质上说面向对象的,你不管如何给一只猴子化妆,他也变不成人。但是对有些不怎么挑剔的观众来说,只要化妆的技术足够高超,就可以把猴子当作人来看。他们(包括我)认为,通过对java现有技术的组合,可以实现对函数式编程的一些我们喜欢的特性的模拟,享受函数式编程的快乐和便利。基于此,java8推出Function包,可以一定程度上让我们感受函数式编程的快乐。关于Function包的使用,可以参看Java 8 Consumer、Supplier、Predicate、Function

言归正传,进入今天的主题。我们通过一个常见的需求,来在讨论函数式编程和面向对象式编程的思想下,分别会有什么样的应对方法。

需求案例

传入一个Long型的id List, 将其转换成加上特定前缀后缀的id key。如id为1对应的id key为 Number_01_cache_key

面向对象编程

面向对象编程,就需要我们把数据(id List)和对数据的操作(convert函数的逻辑)放到同一个对象中去, 因此像下面这样的操作是典型的面向对象的编程过程:

DetailReader_Object_way.java

package ObejectProgramming;

import java.util.ArrayList;
import java.util.List;
/**
 * @author longxingjian
 * Created on 2020-01-14
 */
public class DetailReader_Object_way {
    private List<Long> ids;

    public DetailReader_Object_way(List<Long> ids){
        this.ids = ids;
    }

    public List<String> convert(){
        String format = "Number_%s_CACHE_KEY";
        List<String> convertedCacheKeys = new ArrayList<>();
        for(Long id:ids)
            convertedCacheKeys.add(String.format(format,id));
        return convertedCacheKeys;
    }
}

如上将对id的转换逻辑写到convert操作中,这里面的转换逻辑只为本类的ids数据服务,且本类的ids数据只能使用convert操作中定义的转化逻辑。一旦我们的转换逻辑发生了变更,我们也必须修改业务代码。
下面是测试函数:

ObjectProgrammingTest.java

package ObejectProgramming;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

/**
 * @author longxingjian
 * Created on 2020-01-14
 */
public class ObjectProgrammingTest {
    @Test
    public void objectProgrammingTest(){
        List<Long> ids =  new ArrayList<>();
        ids.add(1L);
        ids.add(2L);

        DetailReader_Object_way detailReader = new DetailReader_Object_way(ids);
        List<String> cacheKeys = detailReader.convert();
        for (String s : cacheKeys) {
            System.out.println(s);
        }
    }
}

结果:

函数式编程

函数式编程的目标是解藕数据和函数服务。在java中对象是提供服务的最小单元,因此一个折衷的办法是,我们定义某一种服务,它只提供类似函数的单一服务,并且以对象传递的形式发送给另一个对象使用,好像我们把这个函数服务单独发送过去了一样,至于接收者怎么使用,那是他们的事情了。
对于函数式的服务, 在java中由于语言的要求必须以一个对象的形式提供,且我们要求它提供单一的服务,只做一件事,这个功能由java8的Funciton包为我们实现。Function对象的公开方法只有一个apply方法,这个对象作为这种服务的容器来使用。话不多说,来看代码:

DetailReader_Functional_Way.java

package FunctionalProgramming;

import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author longxingjian <[email protected]>
 * Created on 2020-01-14
 */
public class DetailReader_Functional_Way {
    private Function<Long, String> cacheKeyFormatter;
    private List<Long> ids;

    public DetailReader_Functional_Way(Function<Long, String> function, List<Long> ids) {
        this.cacheKeyFormatter = function;
        this.ids = ids;
    }

    public List<String> convert() {
        List<String> convertedCacheKeys = ids.stream().map(cacheKeyFormatter).collect(Collectors.toList());
        return convertedCacheKeys;
    }
}

这个对象虽然也有一个convert方法,但是我们看到这个方法没有任何的转换逻辑,它的转换逻辑是通过调用cacheKeyFormatter来实现的,而cacheKeyFormatter正是与数据ids一起定义在开头的一个成员,他是一个Function对象,从泛型中可以看出这个函数式对象提供的函数功能是输入Long型输出String型。
里看看这个对象是怎么传入的:

CacheKeyTest.java

package FunctionalProgramming;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

/**
 * @author longxingjian 
 * Created on 2020-01-14
 */
public class CacheKeyTest {
    @Test
    public void cacheKeyTest() {
        List<Long> ids = new ArrayList<>();
        ids.add(1L);
        ids.add(2L);

        DetailReader_Functional_Way detailReader = new DetailReader_Functional_Way(CacheKeyProvider::getCacheKey, ids);
        List<String> convertedKeys = detailReader.convert();
        for (String key : convertedKeys) {
            System.out.println(key);
        }
    }
}

注意看DetailReader_Functional_Way detailReader = new DetailReader_Functional_Way(CacheKeyProvider::getCacheKey, ids);
这里我们通过函数指针为Function对象传入了CacheKeyProvider 的成员函数getCacheKey
来看看CacheKeyProvider对象是如何定义的:

CacheKeyProvider.java

package FunctionalProgramming;

/**
 * @author longxingjian 
 * Created on 2020-01-14
 */
public class CacheKeyProvider {
    final static String CACHE_KEY = "Number_%S_CACHE_KEY";

    public static String getCacheKey(Long id) {
        return String.format(CACHE_KEY, id);
    }
}

运行CacheKeyTest.java

梳理上述过程,我们发现java函数式编程的结果是通过提供一种特殊的对象--只提供一种服务的对象来实现对函数式编程的模拟。这种方式虽然也是在对象层面的解耦,但也实实在在的将数据与操作分离开来。在上面的实现中,我们可以通过在CacheKeyTest.java传入不同的函数指针而使用不同的函数逻辑,而不需要去修改DetailReader_Functional_Way.java中的业务代码。在DetailReader_Functional_Way.java中,处理逻辑和被处理的数据是放在对等的位置的。

猜你喜欢

转载自www.cnblogs.com/greatLong/p/12191457.html