在Java中使用函式正規化提高程式碼質量

banq發表於2018-11-11

在一個正規化和技術堆疊一直在變化的世界中,保持競爭力和提高生產力和質量的鬥爭有時候證明是一項挑戰。
在本文中,我想首先展示一下函式程式設計(FP)的優勢,特別是加強Java編碼體驗。在嘗試將正規化轉換為函數語言程式設計時,我將嘗試迭代我發現最重要的幾個原因。請記住,這絕不是一個巨大的創新,我相信FP自70年代以來一直存在,但僅在最近幾年它才獲得吸引力並增加了人們的興趣。我們來看看為什麼!

併發
隨著多核/多執行緒處理器的出現,函數語言程式設計開始受到更多關注。這絕不是一個簡單的巧合,因為函數語言程式設計鼓勵使用不可變物件,屬性和變數應該是一種其值不能更改的資料容器)。看看下面程式碼:

private int aNumber;
public void setNumber(int numberParameter){ 
  this.aNumber = numberParameter; 
}

很簡單吧?你以前可能已經看過很多次了。但是如果兩個執行緒同時訪問setNumber方法會發生什麼?你可以想象某種阻塞可能會發生,最後,只有訪問該方法的最後一個執行緒才會對aNumber的值有最終決定權。但這不確定,取決於各種因素,因此,我們可以說方法setNumber不是引用透明的(後面會詳細介紹)。這種情況下不變性有助於推理程式碼,因為我們確信無論有多少執行緒訪問它的一部分,它的值總是相同的。

引用透明和可測試性
函數語言程式設計鼓勵使用引用透明的函式。那是什麼意思?嗯,這意味著一個函式總是可以被它的值替換,一切都將保持不變。看一下以下程式碼塊:

import java.util.Random;
public class RandomValueProvider { 
  public int getSomeRandomValue(){ 
    Random rand = new Random();
    return rand.nextInt(50);
  } 
}

getSomeRandomValue()方法引用透明嗎?試著用它的值替換它,它總是保持不變嗎?可能不會。儘可能嘗試使用引用透明的函式可能是一個好習慣。想象一下,測試上面的getSomeRandomValue方法比測試以下方法要困難得多:

public int getSum(int a,int b){ 
  return a + b; 
}

具有暗示名稱的小函式通常比式樣表示它們返回值的表示式更好。好處是能確保我們編寫的(至少大部分)函式是確定性的。這將增加程式碼推理的簡易性以及可測試性。

函式組合
在應用FP原則時,操作現在更簡單,更具確定性,這一事實使我們能夠透過將不同的功能組合在一起來建立更復雜的行為。將其他函式作為引數或返回函式一起接收的函式稱為高階函式。
這裡的一些示例來自Java 8 Stream API。自2014年成為JDK的一部分(甚至在此之前)以來,已經在流上編寫了大量內容。我只是想在這裡使用Consumer函式介面展示一個簡單的例子:

public void processListOfNumbers(List<Integer> listOfNumbers, Consumer<Integer> processor) {
  return listOfNumbers.stream()
    .forEach(number -> processor.accept(number));
}


客戶端程式碼:

List<Integer> numbers = Arrays.asList(5, 6, 7, 8);
Consumer<Integer> numberPrinter = n -> System.out.println(n);
processListOfNumbers(numbers, numberPrinter);


方法processListOfNumbers是函式組合的一個示例,您可能會聽到它有時被命名為高階函式。在Java中,函式(也包括suppliers, consumers)是物件。這意味著我們可以應用它們,組合它們並將它們作為引數傳遞。

以FP風格編寫的應用程式更加強大
在以函式式編寫程式碼時,應用程式本身的更不容易出錯。這是因為當您的移動一些元件時,應用程式往往變得更容易預測,更容易推理並且更能適應逆境。函式組合和不變性的一般用法將確保所有那些因為應用程式不同部分的狀態變化而導致的錯誤現在預設消失了。該應用程式將更加強大,可以提供更短的開發 - >測試 - >除錯迭代迴圈。

專注於“什麼”而不是“如何”
假設我們有一個getUserById方法(在同一個類中)負責從資料庫中獲取相應的User物件,請使用以下Java流的經典應用程式:

public List<User> getAdultUsers(List<Integer> listOfUserIds) { 
  return listOfUserIds.stream().map(this::getUserById)
    .filter(user -> user.getAge() >= 18)
    .collect(Collectors.toList());
}


現在讓我們看看非函式風格的相同程式碼:

public List<User> getAdultUsers(List<Integer> listOfUserIds) {
  List<User> adultUsers = new ArrayList<>();
  for(int id: listOfUserIds) {
    User user = getUserById(id);
    if (user.getAge() >= 18) {
      adultUsers.add(user);
    }
  }
  return adultUsers;

}

除了第二段略長外,我們還可以注意到這段程式碼需要花時間來“解釋”此操作的每個步驟是如何完成的:建立一個空白列表,迭代id,獲取每個使用者,新增一些基於條件表示式的使用者到空白列表,完成並返回收集的使用者。
另一方面,在第一段中,功能方法更側重於“什麼”。程式碼在做什麼?它將一些ID對映到某些使用者,將其過濾掉並將其餘使用者收集到列表中。有人可能會爭辯說,透過在第二種情況下提取小方法可以實現同樣的目的,但我相信第一段的流和函式作為資料方法仍然更好。它將我們的函式置於業務邏輯的最前沿,具有與在我們的應用程式中移動的任何其他資料相同的狀態。

更好看的方法簽名
當我們的功能從命令式轉變為函式式時,命名也一目瞭然,以下方法很難透過其簽名來閱讀:

public void executeProcess() {
  // executing some mysterious stuff!
}

程式碼做了什麼?為什麼它不想要我們的任何輸入引數,為什麼它不想返回任何結果?你能測試一下嗎?你能讀懂嗎?不容易吧。如果像下面這樣看起來如何?

public ExecutionStatus executeProcess(Process processToBeExecuted) {
  // execute "processToBeExecuted" and return some status
}


只需採用一些FP概念,並在這個簡單的情況下使用它們,程式碼就變得更具可讀性。函式現在是可透過檢視它的方法簽名來說明自己(雖然方法名稱可能仍然可以改進)。它需要一個Process輸入並以某種ExecutionStatus狀態返回。除了直接在程式碼中提供更好的“文件”之外,簽名變得更有意義。執行什麼Process?我們可以檢視Process物件並在執行時檢視它。它發揮作用後會發生什麼?我然後可在我們的流程中使用該函式的返回結果。

結論
如今,無論我們是在處理遺留程式碼還是新建綠地專案,我們都可以使用一些東西來提高日常工作的質量和生產率。函式程式設計從不同的角度進行編碼。它通常意味著更簡潔,但如果給予適當的照顧,也會提高可讀性。它還幫助我們解決一些常見的痛苦,例如併發程式設計中的競爭條件,令人討厭的物件狀態錯誤或難以遵循的程式碼。
 

相關文章