我们开发人员喜欢抽象。 没有它,我们将无法构建应用程序。 我们的编程学科甚至要求我们对抽象进行编码,并避免将我们的代码耦合到详细的实现。
但是,什么是适合您的应用程序的正确抽象呢?
可悲的是,抽象的选择确实来自我们对框架的选择。 框架基本上是我们为解决问题而扩展的抽象解决方案。
不幸的是,诸如Spring Boot之类的框架对使用的线程模型,需要扩展的接口,可能的适用数据存储库以及关于问题空间的各种其他假设都持谨慎态度。 在编写第一行代码之前,这有很多限制。
我们真正想做的是首先探索问题空间。 这就是测试驱动设计的全部内容。 我们编写测试来定义什么是成功的代码。 然后,我们实现代码以通过那些测试。 在编写测试以掩盖需求的过程中,我们随后为应用程序编制了工作代码。 随着时间的流逝,我们可以获得足够的工作代码以作为应用程序发布。
所以这导致我问,我们什么时候测试框架的选择?
固执己见的框架在开发过程中过早强制抽象
好吧,我想我们会请经验丰富的资深人士来做出此选择。 因此,此选择必须正确。 不会因为以下原因:
- 我(或我们的公司)只知道此框架,因此我们正在使用它
- 新闪亮,带有很多流行语,我们必须使用它
- 我的简历有点旧,让我们尝试一些新的东西
- 这个便宜一点
- 建筑相信它在锡上说了什么
无论出于何种原因,测试框架选择的唯一方法是使用它来构建应用程序。 而对于那些喜欢固执己见的框架(例如Spring Boot)的人,请告诉我您首先编写最冒险的方面。 这样,您可以快速发现框架的意见是否与您的问题相符。
令人遗憾的是,即使您使用最危险的方面进行测试,发现框架决策是错误的也会导致大量的代码浪费。 可以说,这浪费了企业很多钱,并可能导致项目失败。
例如,假设我们选择“ Spring Reactive”。 是的,我们可以对各种微服务进行并发异步调用。 我们还可以使用NoSQL数据存储中的最新数据。 这都是一个伟大的决定。 但是,随着时间的流逝,我们意识到我们拥有少量数据,其中数据的完整性非常重要。 我们发现我们想使用关系数据库来解决此问题,然后将JPA合并到该数据库中以便更轻松地进行交互。 但是,我们选择Spring Reactive不允许这样做,因为它要求所有I / O都是异步的(JPA是同步数据库调用)。 好的,是的,我们可以使用Scheduler,但是由于缺少事务,我似乎一直在不断地工作。 数据一致性问题开始加剧,我们错过了最后期限。 现在,我可以扔掉所有的Reactive代码,还是继续努力以希望它们都可以在一起。 我肯定需要调换职位,然后才能开始支持我们。 在下一份工作中,我学习了使用Spring Servlet解决这类问题。
这种情况的另一面也很容易发生。 我们开始是想让Spring Servlet与数据库进行JPA交互。 但是,随着时间的流逝,我们意识到数据库交互主要是只读的。 我们真正想要的是Spring Reactive的异步I / O,以同时从多个微服务和数据存储中收集数据。 不幸的是,由于我们选择了Spring Servlet,因此数据收集太慢了。 我们的工作是使用异步Servlet和生成线程来发出并发请求。 最初可以正常工作,但随着时间的推移,负载会增加。 这大大增加了线程数,导致线程调度不足,导致超时。 如果不对应用程序进行大量重写,我真的无法解决此问题。 在下一份工作中,我学会了使用Spring Reactive解决此类问题。
因此,可以不必去扔掉所有代码就能测试框架吗?
反转框架控制
依赖注入在反转控制方面走了很长一段路。 当我编写Servlet处理方法时,不再需要传入所有依赖对象。 我将通过@Inject定义依赖项,以使框架使它们可用。 随后,该框架不再规定我的实现可以依赖哪些对象。
但是,框架不仅仅是对象,还有很多其他功能。 框架将施加某种线程模型,并要求我扩展某些方法。 尽管依赖注入提供了对对象的引用,但是框架仍然必须调用对象上的方法以执行任何有用的操作。 例如,Spring一直在使方法变得灵活,但仍然通过方法所需的返回类型将您耦合到Reactive或Servlet编码。
由于我需要Spring框架来进行测试的依赖注入,因此在编写第一行代码之前,我就已经耦合到特定的Spring Servlet / Reactive抽象。 如果我弄错了,更改的前期选择可能会非常昂贵!
我真正想做的是:
- 为我的实现编写测试( 当然,因为我们总是受测试驱动 )
- 写我的实现
- 将我的实现连接在一起成为应用程序
好,前两个很简单:
- 编写调用传递模拟对象的方法的测试
- 编写方法的实现以通过测试
最后变得非常困难。 最后一个变得很困难的原因是没有一致的方法来调用每个方法。 方法具有不同的名称,不同的参数,不同的异常,可能不同的线程要求和不同的返回类型。 我们需要的是使这些方法看起来相同的方法的基础。
(耦合)控制的反转(IoC)通过ManagedFunction在方法上提供了这种外观。 ManagedFunction接口不会指示要使用的线程,需要的参数/返回类型,也不会抛出异常。 所有这些都由所包含的方法实现指定。 耦合是反向的,因此实现可指定所需的条件。
耦合的这种反转使得可以推迟框架决策。 因为我可以用一致的方式调用所有方法,所以我可以继续并开始编写实现。 这些实现可能需要响应式编码来对不同的微服务进行异步调用。 其中一些实现可能需要使用JPA写入关系数据库。 我真的不应该在开始构建系统时就在意。 我正在解决具体问题,以更好地了解实际问题空间。 我知道我的方法可以通过将框架包装在ManagedFunction中来调用。 一旦了解更多信息,我们便可以稍后确定合适的框架。
实际上,这允许实现选择框架要提供的适当抽象。 我的实现定义了所需的对象,所需的其他方法以及所需的线程模型。 这些实现有效地定义了从框架中需要什么抽象。
因此,它不再是框架。 允许对您的开发人员代码提出意见。
然后,这可以使您的实现对使用的最合适的框架有意见。 您不再需要基于对问题空间的模糊理解来猜测框架。 您可以看到实现需要什么抽象,并可以更明智地选择框架。
实际上,IoC已将框架的选择推迟到开发过程的后期。 这样,您可以更加自信地做出决定。 这不是敏捷所说的,将承诺推迟到最后一个负责任的时刻。
摘要
总而言之,为什么要被迫对您的应用做出太多的前期决策? 在选择框架时,您正在做出一些重要的选择来解决您的问题空间。 鉴于框架的存在,它们对您的解决方案造成了很多耦合。
相反,为什么我不能立即开始为具体问题编写解决方案并担心以后它们如何融合在一起? 当我对问题空间有更多了解时,这使我可以选择适当的抽象(以及随后的框架)。
当您被告知正确地做出决策时,(耦合)控制的反转使此功能可以将抽象和框架选择推迟到开发过程的后期。
翻译自: https://www.javacodegeeks.com/2019/05/opinionated-frameworks-inverting-opinionated-code.html