内容简介:我从很多人那里收到了同样的问题:为什么你甚至会使用接口?当涉及到接口时,人们倾向于认为他们唯一的用途是当你有多个实现时,你可以轻松地将它们切换出来。然而,大多数人在他们的应用程序中没有特定功能的多个实现。那么为什么你会使用接口呢?在我们的IDE中的所有重构工具都很强大之后我们可以在以后介绍它们......
我从很多人那里收到了同样的问题:为什么你甚至会使用接口?
当涉及到接口时,人们倾向于认为他们唯一的用途是当你有多个实现时,你可以轻松地将它们切换出来。然而,大多数人在他们的应用程序中没有特定功能的多个实现。那么为什么你会使用接口呢?在我们的IDE中的所有重构 工具 都很强大之后我们可以在以后介绍它们......
合同,而不是接口
大多数人都认为接口是这样的:
<b>interface</b> ContentAuthorizer {
<b>boolean</b> authorize(String userId, String contentId);
}
此接口描述了必须实现的方法签名,但没有指示此方法应该如何操作,或者(取决于您的语言)是否接受空值以及抛出哪些异常。因此,实际记录预期行为几乎没有作用。
正如许多人,尤其是质疑接口有用性的人所认识到的那样,这并不是特别有用。如果我们没有多个ContentAuthorizer实现,这没有用。
相反,我想提倡改变哲学。不要将接口视为签名强制执行,而应将其视为合同。他们应该描述执行方必须如何表现以及使用方应注意什么(例如,哪些例外需要捕捉)。
因此,编写上述接口的更好方法是:
<b>interface</b> ContentAuthorizer {
<font><i>/**
* Decide if a certain user can access a certain piece of content and
* return true if the user is allowed to access the content.
*
* @param userId the ID of the user requesting access. Must not be null
* and must contain a valid user ID
* @param contentId the ID of the content that access is requested to.
* Must not be null and must contain a valid content ID.
*
* @return true if the user is allowed to access, false otherwise.
*
* @throws InvalidUserId if the userId parameter is null or of an
* invalid format.
* @throws NoSuchUser if the user specified with the ID is not
* found.
* @throws InvalidContentId if the contentId parameter is null or of
* an invalid format.
* @throws NoSuchContent if the content specified with the ID is
* not found.
*/</i></font><font>
<b>boolean</b> authorize(String userId, String contentId);
}
</font>
哇,这是一个像这样的小功能的很多文字!但是,如果你看一下,我们定义了行为而不是签名。在实施之前,我们考虑了所有失败案例并定义了正确的错误处理。
如果你没有这样做,你有什么机会懒得做正确的例外处理,只是处理一切NullPointerException或者一个InvalidParameterException?有什么机会能找出底层代码抛出的异常?
合同的目的是定义一个内部API,您可以在不考虑底层实现的情况下使用它。就像一份写得很好的法律文件,它确切地说明了各方应该如何表现。
测试
现在,让我们更进一步。让我们假设您不仅需要一个好的结构,而且还想测试您的应用程序。正如 前面所讨论的 写的可能比较容易测试之一是单元测试。
单元测试被称为是因为它测试的单元(或类在我们的例子)隔离。这是什么意思?我们假设我们要测试一个这样的控制器:
<b>class</b> BlogPostController {
<b>public</b> ViewModel getLatestBlogPosts() {
<font><i>//...</i></font><font>
}
}
</font>
这个控制器显然有一些依赖关系,我们当然会 注入这些依赖关系 :
<b>class</b> BlogPostController {
<b>private</b> BlogPostFetchBusinessLogic blogPostFetchbusinessLogic;
<font><i>//...</i></font><font>
<b>public</b> BlogPostController(
BlogPostFetchBusinessLogic blogPostFetchbusinessLogic
</font><font><i>//...</i></font><font>
) {
<b>this</b>.blogPostFetchbusinessLogic = blogPostFetchbusinessLogic;
</font><font><i>//...</i></font><font>
}
</font><font><i>//...</i></font><font>
}
</font>
我们现在有两种情况:要么BlogPostFetchBusinessLogic是接口,要么是实际的实现。让我们来看看两种情况下我们的测试结果如何。首先是接口:
<b>class</b> BlogPostControllerTest {
<b>private</b> BlogPostController createController() {
<b>return</b> <b>new</b> BlogPostController(
<b>new</b> FakeBlogPostFetchBusinessLogic()
);
}
<b>class</b> FakeBlogPostFetchBusinessLogic implements BlogPostFetchBusinessLogic {
<font><i>//...</i></font><font>
}
}
</font>
因此,我们传递了一个实际的,简化的获取业务逻辑实现。这种虚假的业务逻辑没有其他依赖关系,因此为了测试的目的而实例化它相当容易。
现在,当我们实例化实际实现时,相同的代码是如何的?
<b>class</b> BlogPostControllerTest {
<b>private</b> BlogPostController createController() {
<b>return</b> <b>new</b> BlogPostController(
<b>new</b> BlogPostFetchBusinessLogic(
<b>new</b> BlogPostStorage(
<b>new</b> DatabaseConnectionFactory(
<font><i>//database parameters</i></font><font>
)
)
)
);
}
}
</font>
从本质上讲,您正在引入并测试整个应用程序,而不仅仅是控制器。如果任何底层有问题,那么在单元测试中也会出现故障。请记住,单元测试的目的是 准确地指出问题所在。如果30个测试失败,因为您在存储层的某处出现了一个错误,或者您的数据库不可用,那么在追踪问题时这不会非常有用。
总而言之,如果您编写单元测试,则确实有多个相同接口的实现。
代理模式
当你将接口作为工具中的工具包含在你的工具库中时,你也可以用它做很漂亮的技巧。假设您有一个从远程API获取一些数据的类。或者,更准确地说,让我们使用一个接口:
<b>interface</b> MyRemoteDataFetcher {
<font><i>/**
* Fetch the remote data set by ID. As the remote data set is immutable, the method MAY return a cached version.
*
* ...
*/</i></font><font>
MyRemoteDataSet fetchRemoteData(String dataId);
}
</font>
正如您所看到的,接口描述得很好,实际上可以在本地缓存数据,因此不需要每次都重新获取,因为它无论如何都不会被修改。
如果我们现在决定将获取和缓存逻辑全部放在一个严重违反 单一责任原则的类中 。所以,我们可以使用这样的接口:
<b>class</b> MyRemoteDataFetcherImpl implements MyRemoteDataFetcher {
<b>public</b> MyRemoteDataSet fetchRemoteData(String dataId) {
<font><i>//fatch</i></font><font>
}
}
<b>class</b> MyCachingProxyRemoteDataFetcherImpl implements MyRemoteDataFetcher {
<b>private</b> MyRemoteDataFetcher actualFetcher;
<b>public</b> MyCachingRemoteDataFetcherImpl(
MyRemoteDataFetcher actualFetcher
) {
<b>this</b>.actualFetcher = actualFetcher;
}
<b>public</b> MyRemoteDataSet fetchRemoteData(String dataId) {
</font><font><i>//Use the actual fetcher to fetch if the data is not cached</i></font><font>
}
}
</font>
提示:通常,您希望将实现称为更具描述性的内容,例如嵌入实现正在使用的库。一个很好的例子是UnirestRemoteDataFetcher和InMemoryCachingRemoteDataFetcher。
如您所见,接口的一个实现正使用另一个实现。然后我们可以配置我们的依赖注入器将它们链接在一起,让应用程序缓存数据。这样我们就不会违反SRP,如果我们以后决定放入缓存逻辑,我们也不必触及我们的fetcher实现。
告!根据合同的精神,只有在合同允许的情况下才应添加缓存!如果您在没有上层期望的情况下添加缓存,则可能会破坏应用程序!
结论
这里围绕接口列举了几个用例,但我希望你能将它作为一个非常有用的工具包含在你的工具库中。提前考虑并定义内部API可以为您节省大量时间。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Tomcat架构解析
刘光瑞 / 人民邮电出版社 / 2017-5 / 79.00元
本书全面介绍了Tomcat的架构、各组件的实现方案以及使用方式。包括Tomcat的基础组件架构以及工作原理,Tomcat各组件的实现方案、使用方式以及详细配置说明,Tomcat与Web服务器集成以及性能优化,Tomcat部分扩展特性介绍等。读者可以了解应用服务器的架构以及工作原理,学习Tomcat的使用、优化以及详细配置。一起来看看 《Tomcat架构解析》 这本书的介绍吧!