当你设计新的API时你需要做很多决策。一般都是基于大量的设计原则来做出的这些决策。Joshua Bloch在他的
报告中总结了一些。他提到的主要原则有以下这些:
易于学习
易于使用
不易误用
写出的代码易于阅读及维护
足够强大,能满足需求
易于扩展
对使用者友好
从上面这个列表可以看到,Joshua Bloch强调的是可读性和易用性。但这个列表中完全忽略了的一点就是性能。不过性能会影响到你的设计 吗?
要回答这个问题,我们先来设计一个API的简单的用例,然后再测试它的性能。这样我们可以根据结果,来看下API的设计是否会对性能产生影响。作为例子,我们使用的是一个经典的从服务或者存储中加载一个客户列表的用例。我们需要考虑的是并不是所有的用户都能执行这个操作。因此,我们必须实现某种权限检查。要实现这个检查并将信息返回给调用方,我们有好几种实现的方式 。我们首先来尝试下这个:
List<Customer> loadCustomersWithException() throws PermissionDeniedException
这里如果调用方没有权限来获取客户列表的话,我们给它定义了一个显式的异常。方法返回的是一个客户的列表,由于我们假设可以从某个容器或者ThreadLocal的实现中获取到用户,因此在没有在方法参数中传入用户信息。
上述方法的签名很方便使用,同时也很难用错。使用了这个方法的代码看起来会是这样的:
try {
List<Customer> customerList = api.loadCustomersWithException();
doSomething(customerList);
} catch (PermissionDeniedException e) {
handleException();
}
读者很快就会发现,只有当我们没有出现PermissionDeniedException异常的时候,才会返回一个客户列表,我们才会执行后续的操作。不过从影响性能的角度考虑的话,异常的确会消耗一些 CPU时间,因为JVM必须中断正常的代码执行,同时去遍历栈来找出继续执行的位置。如果我们考虑到现代处理器架构的话这个会更加麻烦,因为它们希望能在管道中来执行代码序列。因此出于性能的考虑,使用另一种方式来通知调用方没有权限的话会不会更好一些呢?
第一个想法就是创建另一个方法,在调用这个最终会抛出异常的方法前来检查权限。调用的代码看真起来会是这样的:
if(api.hasPermissionToLoadCustomers()) {
List<Customer> customerList = api.loadCustomers();
doSomething(customerList);
}
这段代码的可读性也不错,不过我们引入了另一个方法调用,它会消耗一定的开销。不过现在可以确定的是不会抛出异常了,因此我们可以省掉这个try/catch块了。但这段代码破坏了易用性的原则,因为现在在一个用例中,我们需要调用两个方法,而不是一个。你要注意别忘了每次获取操作中还有一次额外的方法调用。考虑到整个项目,你的代码到处可能都是这些检查权限的语句。
还有一个解决异常的方法就是提供一个空列表给这个API,让具体实现去填充它。返回值可以是一个布尔值,来表明这个用户是否有权限来执行这个操作,或者来表明如果这个列表是空的,只是因为没有查找到用户。这听起来像C或者C++编程的方式,调用方来进行内存管理,被调用方来使用它。这个方法的话就算你没有权限获取列表,也会有一个空列表的开销,:
List<Customer> customerList = new ArrayList<Customer>();
boolean hasPermission = api.loadCustomersWithListAsParameter(customerList);
if(hasPermission) {
doSomething(customerList);
}
解决这个问题的最后一个方法是返回两段信息给调用方,这需要增加一个新的类,它不仅持有用户列表,还有一个布尔值,来表示用户是否有权限来执行这个操作:
CustomerList customerList = api.loadCustomersWithReturnClass();
if(customerList.isUserHadPermission()) {
doSomething(customerList.getCustomerList());
}
由于我们得创建额外的对象,这样消耗了内存和性能,我们还得处理这个额外的类,而这个类什么也不干 ,只是作为一个简单的数据容器,来提供两段信息而已。尽管这个方法也很容易使用,写出的代码可读性也很强,但是它为了维护这个单独的类,带来了额外的开销,还用了一种很无语的方式来表明返回的这个空列表之所以是空的是因为没有权限。
介绍完这三种不同的方法后,现在该来测试下它们的性能了,一种情况是调用方有权限,另一种是调用方没有权限。下表中的结果是第一种情况执行了1000000次后的结果:
Measurement |
Time[ms] |
testLoadCustomersWithExceptionWithPermission |
33 |
testLoadCustomersWithExceptionAndCheckWithPermission |
34 |
testLoadCustomersWithReturnClassWithPermission |
41 |
testLoadCustomersWithListAsParameterWithPermission |
66 |
正如我们所预想的,使用了额外的类来以及传入空列表的的这两个方法和抛异常的实现相比开销要更大。即使是使用了一个专门的方法来检查权限的这种方式也没比直接调用慢多少。
下表是调用方没有权限来获取列表时的结果:
Measurement |
Time[ms] |
testLoadCustomersWithExceptionNoPermission |
1187 |
testLoadCustomersWithExceptionAndCheckNoPermission |
5 |
testLoadCustomersWithReturnClassNoPermission |
4 |
testLoadCustomersWithListAsParameterNoPermission |
5 |
这个抛出了特定异常的方法比其他方法要慢得多。它影响的量级比你原先想的要高得多。不过从上表中我们也知道这种情况的解决方案了:如果没有权限的情况非常多的话,增加另一个方法来进行权限检查。有没有权限时运行时差异这么大是因为,在这里如果有权限的话我返回的是一个只有一个客户的ArrayList,因此没有权限的话loadCustomer()方法的调用的开销是要昂贵一点。
结论
当性能是一个关键因素的时候,你在设计新的API时也应该考虑到它。从我们上面看到的性能分析来看,最后选择的解决方案可能会破坏了API设计中的易于使用和难以用错的原则。
原创文章转载请注明出处:
http://it.deepinmind.com
英文原文链接