你为什么要使用接口? - 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可以为您节省大量时间。


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

查看所有标签

猜你喜欢:

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

Remote

Remote

Jason Fried、David Heinemeier Hansson / Crown Business / 2013-10-29 / CAD 26.95

The “work from home” phenomenon is thoroughly explored in this illuminating new book from bestselling 37signals founders Fried and Hansson, who point to the surging trend of employees working from hom......一起来看看 《Remote》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具