本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃!
任何寫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處理(導致無所不在的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 }
當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);
出于好的原因,对于任何给定的Java类,只能有一个超类。此外,在API中展示抽象或基类应该由客户端代码继承,这是一个非常大和有问题的API 功能。避免API继承,而考虑提供静态接口方法,采用一个或多个lambda参数,并将那些给定的lambdas应用到默认的内部API实现类。
这也创造了一个更清晰的关注点分离。例如,并非继承公共API类AbstractReader和覆盖抽象的空的handleError(IOException ioe),我们最好是在Reader接口中公开静态方法或构造器,接口使用Consumer cb4f314346501f136be324eca3ec9a0a并将其应用于内部的通用ReaderImpl。
你可以这样写
Reader reader = Reader.builder() .withErrorHandler(IOException::printStackTrace) .build();
而不要这样写
Reader reader = new AbstractReader() { @Override public void handleError(IOException ioe) { ioe. printStackTrace(); } };
使用@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(Functionee2b8263933625d1848a755ff2bdf311 renderer) 和add(Predicate44a45ae2deb400dab67a6f07325591bb 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 ); } }
在历史上,人们一直草率地在确保验证方法输入参数。因此,当稍后发生结果错误时,真正的原因变得模糊并隐藏在堆栈跟踪下。确保在实现类中使用参数之前检查参数的空值和任何有效的范围约束或前提条件。不要因性能原因而跳过参数检查的诱惑。
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); }
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(" "));
以上就是Java 8 API 设计经验浅析 的内容,更多相关内容请关注PHP中文网(www.php.cn)!