内容简介:我从很多人那里收到了同样的问题:为什么你甚至会使用接口?当涉及到接口时,人们倾向于认为他们唯一的用途是当你有多个实现时,你可以轻松地将它们切换出来。然而,大多数人在他们的应用程序中没有特定功能的多个实现。那么为什么你会使用接口呢?在我们的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可以为您节省大量时间。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。