貓頭鷹的深夜翻譯:JAVA8 API設計準則

前言

任何一個寫JAVA代碼的程序員都是一名API設計師!不管是否與他人分享代碼,代碼都將被本身或是他人使用。所以,全部的JAVA開發者都應該瞭解一個好的API設計的基本內容。html

一個好的API設計須要嚴謹的思考和大量的經驗。幸運的是,咱們能夠從其它聰明的人如Ference Mihaly那裏學習到這些。Ference Mihaly的博客啓發了我寫出這篇JAVA8 API附錄。當咱們設計Speedment API時,很是依賴他的清單(我建議全部人都閱讀如下他的指南)。java

從一開始就步入正軌是很重要的,由於一旦API發佈之後,就等於對使用它的客戶發出堅決的承諾。就像Joshua Bloch所說的那樣:「公開的API是永恆的,就像鑽石同樣。你只有一次機會使其成爲正確的設計,因此盡己所能」。一個精心設計的API使堅決而準確的承諾以及實現的高度靈活性結合在一塊兒,並最終惠及API的的設計者和使用者。程序員

爲何要使用清單?設計正確的API(好比,定義一組JAVA類的可見部分)比編寫API背後進行真實操做的實現類要困難的多。這是極少數人才能掌握的藝術。使用清單可使開發人員避免最明顯的錯誤,從而成爲一個更好的開發人員,並節約了大量的時間。面試

強烈建議API設計師從使用者的視角來優化代碼的簡潔性,易用性和一致性 - 而不是去考慮API的具體實現。同時,他們應該儘量隱藏實現的細節。數據庫

不要返回Null說明值的缺失

能夠說,不一致的空處理(致使無處不在的NullPointerException)是JAVA歷史上最大的錯誤來源。一些設計師認爲引入null是計算機領域最大的錯誤。幸運的是,隨着Optional類的出現,在JAVA8中引入了緩解空處理問題的第一步。確保一個可能返回空值的方法用返回Optional代替。segmentfault

這明確的向API用戶說明這個方法可能返回值,也可能不返回值。不要試圖出於提升性能的緣由使用null而不是optional。JAVA8的分析系統會優化大多數的Optional對象。避免在參數和變量中使用Optional。api

正確的作法數組

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

錯誤的作法微信

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

不要使用數組獲取或是發送數據

在JAVA5中引入枚舉時,出現了一個重大的API問題。咱們都知道枚舉類有一個方法values()返回該枚舉類全部值的數組。如今,由於JAVA框架必須確保客戶代碼不會修改枚舉類的值(好比,直接寫入數組),那麼每一個value()方法的調用都會產生內部數組的一個複製數組。數據結構

這樣影響了性能,而且下降了客戶代碼的可用性。若是枚舉類返回了一個不可修改的List,這個List能夠在每一次調用中被複用,那麼客戶端就能夠得到一個更好更可用的枚舉值模型。在一般狀況下,若是API要返回一組對象,能夠考慮提供一個Stream。這能夠說明該值只可讀(而不是具備set()方法的List)。

它還容許客戶代碼輕鬆的收集另外一個數據結構中的元素,而且進行及時的處理。不只如此,API可以惰性初始化元素(好比,從文件,端口或是數據庫中獲取內容)。JAVA8的分析系統會確保在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都是0,咱們能夠返回一個特殊的實現類PointOrigoImpl(該類不包含x或是y域),不然咱們能夠返回另外一個類PointImpl,該類包含x和y域而且值被設置爲傳入值。確保實現類在另外一個包中,而且不是API的一部分(好比將Point放入com.company. product.shape,將實現類放入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繼承而且重寫抽象的方法abstract void 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表達式實現該接口。它也確保了隨着時間推移,該接口仍然能夠用於lambda表達式中,防止抽象方法在之後被意外的添加到API中。

正確作法

@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歧義。好比,若是有兩個方法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);
}

避免在接口中過分使用default方法

能夠很方便的在接口中添加default方法,在某些時候這樣作是有意義。好比,一個方法應當對全部類都相同,而且有一個短小且基礎的實現,那麼它就能夠做爲接口中的一個默認方法。並且,當接口擴展的時候,有時候爲了向後兼容性能夠提供默認接口方法。

如咱們所知,功能接口只包含一個抽象方法,因此當必須添加其它方法時,默認方法提供了一個解決方法。可是,應當避免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不該當接收空值可是殊不知爲什麼接收了,用戶會感到困惑。

正確的作法

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()

JAVA8的設計者在爲Optional.get()方法命名時犯了個錯誤,它應當稱做Optional.getOrThrow()或是相似的名字。調用get()方法卻不用Optional.isPresent()方法檢查值是否存在是一個很常見的錯誤,它嚴重違背了Optional類的初衷。可使用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(" "));

參考內容

使用 Optional 處理 null
Java8 如何正確使用 Optional

clipboard.png
想要了解更多開發技術,面試教程以及互聯網公司內推,歡迎關注個人微信公衆號!將會不按期的發放福利哦~

相關文章
相關標籤/搜索