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 <longxingjian@kuaishou.com>
 * 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中,處理邏輯和被處理的數據是放在對等的位置的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM