Java 8 API 設計經驗淺析

2016-11-08    分類:JAVA開發、程式設計開發、首頁精華1人評論發表於2016-11-08

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

任何寫Java程式碼的人都是API設計師!無論編碼者是否與他人共享程式碼,程式碼仍然被使用:要麼其他人或他們自己使用,要麼兩者皆有。因此,對於所有的Java開發人員來說,瞭解良好API設計的基礎很重要。

一個好的API設計需要仔細思考和大量的經驗。幸運的是,我們可以從其他更聰明的人,如Ference Mihaly——正是他的部落格啟發我寫了這篇Java 8 API附錄——那裡得到學習。在設計Speedment API時,我們非常依賴於他列出的介面清單。(我建議大家不妨讀一讀他的指南。)

從一開始就做到這一點很重要,因為一旦API釋出,就會成為使用API的人堅實的基石。正如Joshua Bloch曾經說過的:“公共API,就像鑽石一樣永恆久遠。你有機會把它做正確的話,就應該竭盡全力去做。”

一個精心設計的API結合了兩個世界的精華,既是堅實而精確的基石,又具有高度的實施靈活性,最終讓API設計師和API使用者受益。

至於為什麼要使用介面清單?正確地獲取API(即定義Java類集合的可見部分)比編寫構成API背後實際工作的實現類要困難得多。它是一個真的很少有人掌握的藝術。使用介面清單允許讀者避免最明顯的錯誤,成為更好的程式設計師和節省大量的時間。

強烈建議API設計者將自己置於客戶端程式碼的角度,並從簡單性,易用性和一致性方面優化這個檢視——而不是考慮實際的API實現。同時,他們應該儘量隱藏儘可能多的實現細節。

不要用返回Null來表示一個空值

可以證明,不一致的null處理(導致無處不在的NullPointerException)是歷史上Java應用程式錯誤最大的唯一來源。一些開發人員將引入null概念當作是電腦科學領域犯的最糟糕的錯誤之一。幸運的是,減輕Java null處理問題的第一步是在Java 8中引入了Optional類。確保將返回值為空的方法返回Optional,而不是null。

這向API使用者清楚地表明瞭該方法可能返回值,也可能不返回值。不要因為效能原因的誘惑使用null而不使用Optional。反正Java 8的轉義分析將優化掉大多數Optional物件。避免在引數和欄位中使用Optional。

你可以這樣寫

public Optional<String> getComment() {
    return Optional.ofNullable(comment);
}

而不要這樣寫

public String getComment() {
    return comment; // comment is nullable
}

不要將陣列作為API的傳入引數和返回值

當Java 5中引入Enum概念時,出現了一個重大的API錯誤。我們都知道Enum類有一個名為values()的方法,用來返回所有Enum不同值的陣列。現在,因為Java框架必須確保客戶端程式碼不能更改Enum的值(例如,通過直接寫入陣列),因此必須得為每次呼叫value()方法生成內部陣列的副本。

這導致了較差的效能和較差的客戶端程式碼可用性。如果Enum返回一個不可修改的List,該List可以重用於每個呼叫,那麼客戶端程式碼可以訪問更好和更有用的Enum值的模型。在一般情況下,如果API要返回一組元素,考慮公開Stream。這清楚地說明了結果是隻讀的(與具有set()方法的List相反)。

它還允許客戶端程式碼容易地收集另一個資料結構中的元素或在執行中對它們進行操作。此外,API可以在元素變得可用時(例如,從檔案,套接字或從資料庫中拉入),延遲生成元素。同樣,Java 8改進的轉義分析將確保在Java堆上建立實際最少的物件。

也不要使用陣列作為方法的輸入引數,因為——除非建立陣列的保護性副本——使得有可能另一個執行緒在方法執行期間修改陣列的內容。

你可以這樣寫

public Stream<String> comments() {
    return Stream.of(comments);
}

而不要這樣寫

public String[] comments() {
    return comments; // Exposes the backing array!
}

考慮新增靜態介面方法以提供用於物件建立的單個入口點

避免允許客戶端程式碼直接選擇介面的實現類。允許客戶端程式碼建立實現類直接建立了一個更直接的API和客戶端程式碼的耦合。它還使得API的基本功能更強,因為現在我們必須保持所有的實現類,就像它們可以從外部觀察到,而不僅僅只是提交到介面。

考慮新增靜態介面方法,以允許客戶端程式碼來建立(可能為專用的)實現介面的物件。例如,如果我們有一個介面Point有兩個方法int x() 和int y() ,那麼我們可以顯示一個靜態方法Point.of( int x,int y) ,產出介面的(隱藏)實現。

所以,如果x和y都為零,那麼我們可以返回一個特殊的實現類PointOrigoImpl(沒有x或y欄位),否則我們返回另一個儲存給定x和y值的類PointImpl。確保實現類位於另一個明顯不是API一部分的另一個包中(例如,將Point介面放在com.company。product.shape中,將實現放在com.company.product.internal.shape中)。

你可以這樣寫

Point point = Point.of(1,2);

而不要這樣寫

Point point = new PointImpl(1,2);

青睞功能性介面和Lambdas的組合優於繼承

出於好的原因,對於任何給定的Java類,只能有一個超類。此外,在API中展示抽象或基類應該由客戶端程式碼繼承,這是一個非常大和有問題的API 功能。避免API繼承,而考慮提供靜態介面方法,採用一個或多個lambda引數,並將那些給定的lambdas應用到預設的內部API實現類。

這也創造了一個更清晰的關注點分離。例如,並非繼承公共API類AbstractReader和覆蓋抽象的空的handleError(IOException ioe),我們最好是在Reader介面中公開靜態方法或構造器,介面使用Consumer <IOException>並將其應用於內部的通用ReaderImpl。

你可以這樣寫

Reader reader = Reader.builder()
    .withErrorHandler(IOException::printStackTrace)
    .build();

而不要這樣寫

Reader reader = new AbstractReader() {
    @Override
    public void handleError(IOException ioe) {
        ioe. printStackTrace();
    }
};

確保你將@FunctionalInterface註解新增到功能性介面

使用@FunctionalInterface註解標記的介面,表示API使用者可以使用lambda實現介面,並且還可以通過防止抽象方法隨後被意外新增到API中來確保介面對於lambdas保持長期使用。

你可以這樣寫

@FunctionalInterface
public interface CircleSegmentConstructor {
    CircleSegment apply(Point cntr, Point p, double ang);
    // abstract methods cannot be added
}

而不要這樣寫

public interface CircleSegmentConstructor {
    CircleSegment apply(Point cntr, Point p, double ang);
    // abstract methods may be accidently added later
}

避免使用功能性介面作為引數的過載方法

如果有兩個或更多的具有相同名稱的函式將功能性介面作為引數,那麼這可能會在客戶端側導致lambda模糊。例如,如果有兩個Point方法add(Function<Point, String> renderer) 和add(Predicate<Point> logCondition),並且我們嘗試從客戶端程式碼呼叫point.add(p -> p + “ lambda”) ,那麼編譯器會無法確定使用哪個方法,併產生錯誤。相反,請根據具體用途考慮命名方法。

你可以這樣寫

public interface Point {
    addRenderer(Function<Point, String> renderer);
    addLogCondition(Predicate<Point> logCondition);
}

而不要這樣寫

public interface Point {
    add(Function<Point, String> renderer);
    add(Predicate<Point> logCondition);
}

避免在介面中過度使用預設方法

預設方法可以很容易地新增到介面,有時這是有意義的。例如,想要一個對於任何實現類都期望是相同的並且在功能上要又短又“基本”的方法,那麼一個可行的候選項就是預設實現。此外,當擴充套件API時,出於向後相容性的原因,提供預設介面方法有時是有意義的。

眾所周知,功能性介面只包含一個抽象方法,因此當必須新增其他方法時,預設方法提供了一個安全艙口。然而,通過用不必要的實現問題來汙染API介面以避免API介面演變為實現類。如果有疑問,請考慮將方法邏輯移動到單獨的實用程式類和/或將其放置在實現類中。

你可以這樣寫

public interface Line {
    Point start();
    Point end();
    int length();
}

而不要這樣寫

public interface Line {
    Point start();
    Point end();
    default int length() {
        int deltaX = start().x() - end().x();
        int deltaY = start().y() - end().y();
    return (int) Math.sqrt(
        deltaX * deltaX + deltaY * deltaY
        );
    }
}

確保在執行之前進行API方法的引數不變數檢查

在歷史上,人們一直草率地在確保驗證方法輸入引數。因此,當稍後發生結果錯誤時,真正的原因變得模糊並隱藏在堆疊跟蹤下。確保在實現類中使用引數之前檢查引數的空值和任何有效的範圍約束或前提條件。不要因效能原因而跳過引數檢查的誘惑。

JVM能夠優化掉冗餘檢查併產生高效的程式碼。好好利用Objects.requireNonNull()方法。引數檢查也是實施API約定的一個重要方法。如果不想API接受null但是卻做了,使用者會感到困惑。

你可以這樣寫

public void addToSegment(Segment segment, Point point) {
    Objects.requireNonNull(segment);
    Objects.requireNonNull(point);
    segment.add(point);
}

而不要這樣寫

public void addToSegment(Segment segment, Point point) {
    segment.add(point);
}

不要簡單地呼叫Optional.get()

Java 8的API設計師犯了一個錯誤,在他們選擇名稱Optional.get()的時候,其實應該被命名為Optional.getOrThrow()或類似的東西。呼叫get()而沒有檢查一個值是否與Optional.isPresent()方法同在是一個非常常見的錯誤,這個錯誤完全否定了Optional原本承諾的null消除功能。考慮在API的實現類中使用任一Optional的其他方法,如map(),flatMap()或ifPresent(),或者確保在呼叫get()之前呼叫isPresent()。

你可以這樣寫

Optional<String> comment = // some Optional value 
String guiText = comment
  .map(c -> "Comment: " + c)
  .orElse("");

而不要這樣寫

Optional<String> comment = // some Optional value 
String guiText = "Comment: " + comment.get();

考慮在不同的API實現類中分行呼叫介面

最後,所有API都將包含錯誤。當接收來自於API使用者的堆疊跟蹤時,如果將不同的介面分割為不同的行,相比於在單行上表達更為簡潔,而且確定錯誤的實際原因通常更容易。此外,程式碼可讀性將提高。

你可以這樣寫

Stream.of("this", "is", "secret") 
  .map(toGreek()) 
  .map(encrypt()) 
  .collect(joining(" "));

而不要這樣寫

Stream.of("this", "is", "secret").map(toGreek()).map(encrypt()).collect(joining(" "));

譯文連結:http://www.codeceo.com/article/java-8-api-design.html
英文原文:API Design with Java 8
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章