你为什么要使用接口? - Janos Pasztor

栏目: 编程工具 · 发布时间: 5年前

内容简介:我从很多人那里收到了同样的问题:为什么你甚至会使用接口?当涉及到接口时,人们倾向于认为他们唯一的用途是当你有多个实现时,你可以轻松地将它们切换出来。然而,大多数人在他们的应用程序中没有特定功能的多个实现。那么为什么你会使用接口呢?在我们的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可以为您节省大量时间。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入理解C++11

深入理解C++11

Michael Wong、IBM XL编译器中国开发团队 / 机械工业出版社 / 2013-6 / 69.00元

《深入理解C++11:C++11新特性解析与应用》内容简介:国内首本全面深入解读C++11新标准的专著,由C++标准委员会代表和IBM XL编译器中国开发团队共同撰写。不仅详细阐述了C++11标准的设计原则,而且系统地讲解了C++11新标准中的所有新语言特性、新标准库特性、对原有特性的改进,以及如何应用所有这些新特性。 《深入理解C++11:C++11新特性解析与应用》一共8章:第1章从设计......一起来看看 《深入理解C++11》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

SHA 加密
SHA 加密

SHA 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具