
了解 API 技術:REST、GraphQL 和異步 API 的比較分析
return Optional.ofNullable(comment);
}
不要這樣做:
public String getComment() {
return comment; // comment is nullable
}
在 Java 5 中引入 Enum
概念時,確實存在一個顯著的 API 設計缺陷。Enum
類中的 values()
方法返回一個包含所有枚舉值的數組。為了確保客戶端代碼不能通過直接修改數組來更改枚舉的值,Java 框架每次調用 values()
方法時都必須生成一個內部數組的副本。
這種設計的缺陷在于它不僅導致性能下降,還影響了客戶端代碼的可用性。如果 values()
方法返回的是一個不可修改的 List
,則這個 List
可以在每次調用中重用,使得客戶端代碼能夠訪問更好、更有用的枚舉值模型。
在設計 API 時,如果需要返回一個元素集合,建議使用 Stream
而不是數組。Stream
的使用能夠明確表示結果是只讀的(不同于具有 set()
方法的 List
),并且它允許客戶端代碼更方便地將元素收集到其他數據結構中,或即時對這些元素進行操作。
此外,API 還可以在元素可用時延遲生成它們,例如從文件、套接字或數據庫中逐步拉取數據。這樣不僅能提高代碼的靈活性,還能減少內存消耗,尤其是通過 Java 8 中改進的逃逸分析,確保在 Java 堆上實際創建的對象最少。
同樣重要的是,避免在方法的輸入參數中使用數組。除非對數組進行防御性復制,否則在方法執行期間,其他線程有可能修改數組的內容,這會帶來潛在的線程安全問題。
通過采用不可修改的集合或 Stream
作為返回值和輸入參數,API 設計能夠提升性能、安全性和可用性,使得客戶端代碼更為簡潔、可靠。
這樣做:
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
,該類保存給定的 x
和 y
值。同時,確保實現類位于一個明顯不屬于API 一部分的包中,例如將 Point
接口放在 com.company.product
包中,而其實現類放在 com.company.product.internal.shape
包中。
這樣做:
Point point = Point.of(1, 2);
不要這樣做:
Point point = new PointImpl(1, 2);
相較于繼承,更推薦使用函數接口和 Lambda 表達式的組合。Java 語言規定,任何給定的類只能有一個超類。此外,在 API 中公開需要由客戶端代碼繼承的抽象類或基類,這將是一個重大且具有潛在問題的 API 設計承諾。因此,應完全避免在 API 中使用繼承,而是考慮提供靜態接口方法,這些方法接受一個或多個 Lambda 參數,并將這些給定的 Lambda 應用于默認的內部 API 實現類。
這種做法也有助于實現更清晰的關注點分離。例如,與其從公共 API 類 AbstractReader
繼承并重寫抽象方法 void handleError(IOException ioe)
,不如在 Reader
接口中公開靜態方法或構建器,該方法接受 Consumer
并將其應用于內部的泛型 ReaderImpl
。
這樣做:
Reader reader = Reader.builder()
.withErrorHandler(IOException::printStackTrace)
.build();
不要這樣做:
Reader reader = new AbstractReader() {
@Override
public void handleError(IOException ioe) {
ioe.printStackTrace();
}
};
使用 @FunctionalInterface
注解標記接口,可以向 API 用戶表明他們可以使用 Lambda 表達式來實現該接口。此外,通過防止在后續開發中意外地向接口中添加抽象方法,它還可以確保接口在一段時間內對 Lambda 表達式保持可用性。
這樣做:
@FunctionalInterface
public interface CircleSegmentConstructor {
CircleSegment apply(Point cntr, Point p, double ang);
// 不能再添加抽象方法
}
不要這樣做:
public interface CircleSegmentConstructor {
CircleSegment apply(Point cntr, Point p, double ang);
// 后續可能會意外添加抽象方法
}
當有兩個或更多具有相同名稱的函數且它們以函數接口作為參數時,這可能會導致客戶端代碼中的 Lambda 表達式產生歧義。例如,如果有兩個 Point
方法 add(Function renderer)
和 add(Predicate 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 接口因不必要的實現細節而變得復雜,進而演變為實現類。如果有疑問,可以考慮將方法邏輯移至單獨的實用程序類,或將其放入實現類中。
這樣做:
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);
}
}
在歷史上,確保驗證方法輸入參數的工作往往被忽視,這導致當錯誤發生時,問題的根本原因常常被掩蓋在堆棧跟蹤的深處。因此,在使用實現類中的參數之前,應該檢查參數是否為空,并確保符合任何有效的范圍約束或前提條件。不要因為性能原因而跳過這些參數檢查。
JVM 能夠優化冗余檢查并生成高效的代碼,建議使用 Objects.requireNonNull()
方法。參數檢查也是確保 API 契約的重要手段。如果 API 不應該接受空值但卻沒有進行驗證,用戶將會感到困惑。
這樣做:
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);
}
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 用戶提供堆棧跟蹤時,如果流管道被分成多行,相比于單行表示的流管道,通常更容易確定錯誤的實際原因。同時,這也提高了代碼的可讀性。
這樣做:
Stream.of("this", "is", "secret")
.map(toGreek())
.map(encrypt())
.collect(joining(" "));
不要這樣做:
Stream.of("this", "is", "secret").map(toGreek()).map(encrypt()).collect(joining(" "));