您的位置:首页 > 编程语言 > Java开发

使用Java8进行API设计

2017-09-03 23:00 288 查看
英文原文:https://dzone.com/articles/the-java-8-api-design-principles

只要你在写Java代码你就是一个API接口的设计者!程序员分不分享他们的代码不重要,代码会一直被其他人或代码的作者使用。因此,Java开发者掌握优秀API设计的基本理念就很重要了。

一个好的API设计需要严谨的思维和很多经验之谈。幸运的是,我们可以向其他聪明的人学习,比如Ference Mihaly,他的指南鼓励我写下这篇Java 8 API补充的文章。笔者在设计 Speedment API的时候,就参考了他清单(checklist)里的很多内容(我建议你去读读他的指南)。

在一开始就把API设计做好是非常重要的,API一旦发布,就对要使用这个API的人做了一个强的约定。Joshua Bloch曾说过:“公共API像砖石一样,象征着永恒。你只有一次做好的机会,所以最好尽全力!”一个好的API设计结合了两个美好世界——稳固和精确的约定下高度灵活性的实现,最终受益是API的设计者和API的使用者。

为什么使用一个清单(checklist)呢?将API(比如在一个明显区域定义一些Java类)定义得准确要比去编写实现逻辑要难得多。这是一门很少人掌握的艺术,使用一个清单让读者能避免大部分明显的错误,节约时间,成为有更好的开发者。

鼓励API设计者将他们自己置身于客户端的调用中,从简洁性、易用性、一致性去优化API的展现方式,而不是思考API的实现逻辑,与此同时,他们应该尽可能地屏蔽掉很多的实现细节。

不要返回Null去表示数据不存在

不一致的null处理(导致无所不在的空指针异常)是很多Java应用的历史性问题。一些开发人员认为null概念的引入是计算机科学领域中最严重的错误之一。幸运的是,Java8 Optional的到来就是为了缓解null处理的“伤痛”的。保证一个方法在没有值的时候使用Optional去代替null。

这给API的用户一个明显的提示就是这个方法可能返回值、也可能没有返回值。不要因为性能原因的驱使就使用null而不是Optional。Java8的逃逸分析(escape analysis)会去优化Optional对象。另外,在参数和字段里要避免使用Optional对象。

Do This:

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


Don’t Do This:

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


在API里不要使用数组去传递值

Java 5中引入Enum概念时,出现一个严重的API错误。我们知道一个Enum class有一个方法values()返回一个数组包含所有的枚举值。现在,因为Java框架必须保证客户端代码不能改变Enum的枚举值(比如,直接往数组里写数据),因此在调用values()方法的时候,内部数组必须生成一份副本以防被外部客户端调用时被改变。

这样做的话,性能和代码的可用性都比较差,如果Enum可以返回一个不可以被修改的List,这个List可以在每个调用中被重用,而客户端代码可以访问一个更好的、更有用的枚举值模型。通常情况下,如果API是返回一个数据集合,可以考虑暴露Stream,很明显的表示返回的结果集是只读的(这和一个有set()方法的List是截然不同的)。

它还允许客户端代码轻松地在另一个数据结构中收集元素,或者动态地执行这些元素。此外,只有API在使用的情况下才加载元素(例如,从文件、socket或数据库中获取数据)。还有就是Java 8改进了逃逸分析(escape analysis),确保在Java堆中创建最少的对象。

也不要使用数组作为方法的输入参数。因此,除非该数组的防御副本被创建,然后在方法执行过程中,另一个线程可以修改数组的内容。

Do This:

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


Don’t Do This:

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字段),或者返回另一个x和y字段都有值的实现类PointImpl。保证API的实现类在另一个包下,并且不是API的一部分(例如Point接口在
com.company.product.shape
,实现类在
com.company.product.internal.shape
)。

Do This:

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


Don’t Do This:

Point point = new PointImpl(1,2);


函数式接口和lambda表达式的组合优于继承

出于充分的理由,任何一个给定的Java类都只能有一个超类。此外,在你的API里暴露抽象类或基类去让客户端代码去继承,这是一个非常有问题的API约定。应该避免使用API继承,而是考虑提供静态接口方法,这些方法使用一个或多个lambda参数,然后使用lambda表达式传递一个默认的内部API实现类。

这样也有了更清晰的关注点分离。例如,不是去继承公共API类AbstractReader和重写抽象方法
void handleError(IOException ioe)
,Reader接口最好暴露一个静态方法或者一个接收Consumer的builder,然后将其应用于内部通用的ReaderImpl。

Do This:

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


Don’t Do This:

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


保证在函数式接口上加上@FunctionalInterface注解

在一个接口上有@FunctionalInterface的注解告诉用户可以使用lambda表达式去实现这个接口,并且保证这个接口可以使用lambda表达式一直重用,同时也为了防止后续有抽象方法被加入到这个API中来。

Do This:

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


Don’t Do This:

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")
,编译器不能确定调用哪个方法,然后就报错了。取而代之,函数名子应该根据其功能择优而取。

Do This:

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


Don’t Do This:

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


避免在接口里过度使用默认方法

默认方法可以很容易的添加到接口中,有的时候这也是很有意义的。例如,这个方法简短,属于基本功能,并且在任何实现类里这个方法都是一样的,那这个方法定义成接口的默认方法是再好不过了。另外,当一个API被扩展时,为了向后兼容的理由提供一个默认的接口方法有时也是有意义的。

总所周知,函数式接口包含一个抽象的方法,当有额外的方法必须要添加时,默认方法是一个解决方案。然而,为了避免一个接口演化成一个实现类,做了不必要的实现。有了这些怀疑,那就考虑将这些方法逻辑放到一个工具类里或在实现类里实现之。

Do This:

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


Don’t Do This:

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方法在执行之前检查参数变量

从漫长的历史中来看,人们在校验方法输入参数是有些草率的。因此,尔后发生错误时,真正的原因就会变得模糊,并隐藏在堆栈跟踪中。确保在实现类中使用参数之前,对null和任何有效范围约束或先决条件进行检查,不要因为性能的原因而跳过方法参数的检查。

JVM会优化掉冗余检查,并生成高效的代码。充分利用
Objects.requireNonNull()
方法,参数检查也是让客户端代码执行API协议的一种重要方式。如果API本是不应该接受null参数的,但是却可以传入null,这会让用户困惑的。

Do This:

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


DON’T DO THIS:

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


不要只是简单的调用下Optional.get()

Java8的API设计者犯了一个错误,在对
Optional.get()
进行命名的时候应该可以命名为
Optional.getOrThrow()
或类似的名字。调用get()方法的时候都没有检查是否有值存在,虽然可以调用
Optional.isPresent()
,但这却是一个错误的方式,完全忽略了Optional当初是为了减少null判断的承诺。因此建议在API的实现类里使用Optional的其它方法比如
map()
flatMap()
或者
ifPresent()
,或者在调用
get()
方法之前保证
isPresent()
被调用过。

Do This:

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


Don’t Do This:

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


在实现类里将你的Stream Pipeline分成多行

最后,所有API都将会包含错误,当用户调用的时候收到了错误堆栈信息,如果Stream pipeline是分成多行的,而不是一行表示所有的pipeline,那么确定错误的真实原因就会容易得多,同时也提升了代码可读性。

Do This:

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


Don’t Do This:

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


扩展阅读:

Design a Fluent API in Java

Better API Design With Java 8 Optional

API First - Microservices
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  api api设计 java8