用懶載入等函式式思想重構Java的初始化
假設有一個簡單的程式來管理儲存在本地檔案系統上的某些檔案的後設資料,使用者可從磁碟讀取這些檔案並以某種方式處理它們。
管理檔案後設資料的類:
@Setter @Getter public class DataFileMetadata { private long customerId; private String type; private File f; private String contents; public void loadContents(){ try { contents = loadFromFile(); }catch(IOException e){ throw new DataFileUnavailableException(e); } } private String loadFromFile() throws IOException { return new String(Files.readAllBytes(f.toPath())); } |
在這個類中,一切都是可變的,contents內容可能是空,或可能是讀取的檔案位元組內容。loadContents委託loadFromFile從檔案中不斷讀取變化的內容。
驚人的複雜性
程式碼只有20行,卻複雜得多。getContents方法和loadContents方法之間存在相互依賴關係。在訪問內容之前需要呼叫loadContents
我們可以建立一個FileManager類,它接受DataFileMeta來處理並透過customerId將它們的內容儲存在HashMap中。
public class FileManager { private Map<Long,String> dataTable = new HashMap<>(); public void process(DataFileMetadata metadata){ dataTable.put(metadata.getCustomerId(),metadata.getContents()); } } |
如果您要提交此程式碼以進行程式碼審查,您可能會收到一些資訊性反饋,即對FileManager :: process的呼叫將導致將空值儲存在HashMap中,並且您需要在process方法中呼叫metadata.loadContents (
metadata是 |
if(metadata.getContents()==null) metadata.loadContents(); |
如果你曾經在一個龐大而複雜的應用程式中工作,其中程式碼審查一般可能是功能交付的瓶頸,應用程式碼中只有少數深度專家可以充分評估程式碼更改的影響 - 這種型別的邏輯和編碼風格往往是核心原因:我們也未能正確地封裝兩種方法之間的核心關係,造成抽象洩漏。
重構
DataFileMetadata類 |
private String contents = loadContents; private void loadContents(){ try { contents = loadFromFile(); }catch(IOException e){ throw new DataFileUnavailableException(e); } } |
將loadContents方法從public變成private,檔案內容載入到記憶體中,直接賦予contents = loadContents,這樣杜絕了getContents方法。它也會造成效能損失,因為我們現在在建立後設資料物件時將每個檔案載入到記憶體中。
函式概念:Laziness
可以過載get方法來解決在建立物件時將每個檔案載入到記憶體中的效能問題:
private String contents = loadContents; public String getContents(){ if (contents == null) loadContents(); return contents; } private void loadContents(){ try { contents = loadFromFile(); }catch(IOException e){ throw new DataFileUnavailableException(e); } } |
再進一步,每次呼叫getContents時我們也不需要從磁碟載入內容,我們可以引入空檢查以僅在第一次呼叫時載入內容。
現在我們已經成功封裝了DataFileMetadata類的檔案管理方面,並透過引入懶惰來獲得可接受的效能。我們以強制性的方式實現了所有這些,因此程式碼相當冗長。我們可以透過引入一些函式型別來清理這些冗長程式碼
Supplier :laziness
JDK的Supplier介面是一個SAM函式介面,,代表了可以在未來懶載入內容,我們可以定義一個指向從File載入資料的方法的Supplier例項:
Suppilier <String> contents = this :: loadFromFile;
也就是說,我們可以建立一個指向用於從檔案載入資料的方法的Supplier型別,loadContents方法的返回型別需要重構為返回一個String,並直接返回檔案的內容。
private String loadContents(){ try { return loadFromFile(); }catch(IOException e){ throw new DataFileUnavailableException(e); } } |
前面呼叫的程式碼重構為:
private Suppilier <String> contents = this::loadFromFile; public String getContents(){ return contents.get(); } |
但是,現在我們已經失去了快取!
函式:快取
Memoization與函數語言程式設計中的懶惰密切相關,並且指的是快取和重新使用延遲計算值的能力。實現Memoization非常簡單,在Java中使用ConcurrentHashMap,我們可以為Supplier 定義一個Memoization函式:
public static <T> Supplier<T> memoizeSupplier(final Supplier<T> s) { final Map<Long,T> lazy = new ConcurrentHashMap<>(); return () -> lazy.computeIfAbsent(1l, i-> s.get()); } |
透過MemoizeSupplier方法傳遞的Supplier都會自動快取它的結果
int called = 0;
Supplier<Integer> lazyCaching = memoizeSupplier(()->called++);
一旦至少一次呼叫lazyCaching.get(),called結果將始終保持為1。但是lazyCaching.get()一直是0;
Cyclops庫包
用於Java函式程式設計的cyclops庫為我們提供了一個實現,我們可以將它新增到我們的Maven Gradle類路徑中:
<dependency>
<groupId>com.oath.cyclops</groupId>
<artifactId>cyclops</artifactId>
<version>10.0.1</version>
</dependency>
呼叫程式碼:
private Suppilier <String> contents = Memoize.memoizeSupplier(this::loadFromFile); |
cyclops 有一個資料型別在快取和非快取情況統一處理懶載入:Eval:
private Suppilier <String> contents = Eval.later(this::loadFromFile); |
在我們的初始實現中,contents欄位必須是可變的,以支援它的懶惰載入(透過getContents方法)。現在,contents可以(應該)變成不可變的了,只有在第一次呼叫getContents方法時,contents才會從磁碟上延遲載入。contents一旦載入就會被快取,所以我們只從磁碟載入一次。
相關文章
- 懶函式函式
- 封裝一個元件 + 函式惰性思想(重寫應用)封裝元件函式
- 【C++】初始化列表建構函式VS普通建構函式C++函式
- QImage:使用QImage建構函式載入影像和使用成員函式loadFromData載入影像的區別函式
- C++入門記-建構函式和解構函式C++函式
- 程式碼重構:函式重構的 7 個小技巧函式
- 重構 - 用各種方式優化自己的函式庫優化函式
- Java 函式式介面 lamada 應用Java函式
- static變數,static程式碼塊,建構函式,程式碼塊等的載入順序變數C程式函式
- Java static變數、作用域、建構函式初始化順序Java變數函式
- 人工智慧---神經網路啟用函式恆等函式、sigmoid函式、softmax函式詳解人工智慧神經網路函式Sigmoid
- 類的建構函式和解構函式函式
- 重構 - 保持函式的單一職責函式
- 實用函式式 Java (PFJ)簡介函式Java
- C++——建構函式之初始化列表C++函式
- 函式式DDD架構入門 - SCOTT WLASCHIN函式架構
- java的基本函式(介紹)--過載概念、基本應用Java函式
- XML DOM 載入函式概述XML函式
- Java建構函式詳解Java函式
- Java泛型建構函式Java泛型函式
- 建構函式與解構函式函式
- 去重函式unique,sort,erase的應用函式
- 預設建構函式、引數化建構函式、複製建構函式、解構函式函式
- 過載運算子、解構函式函式
- Java函式式介面Java函式
- C# 建構函式 (初始化成員變數的角色)C#函式變數
- 關於python建構函式的過載Python函式
- C++ 建構函式和解構函式C++函式
- golang中的init初始化函式Golang函式
- 關於建構函式與解構函式的分享函式
- Java建構函式的繼承問題Java函式繼承
- 建構函式與普通函式的區別函式
- C++ 類建構函式初始化列表介紹C++函式
- Java 學習(09)--方法過載/建構函式/static 變數Java函式變數
- c++中使用建構函式初始化列表的情況C++函式
- javascript中初始化建構函式時new所起的作用JavaScript函式
- static程式碼塊、構造程式碼塊、建構函式以及Java類初始化順序C程式函式Java
- 主建構函式有啥用函式