一样,你做不到! -使用接口和依赖项注入进行长期设计

大家好!

我们终于有了一份合同,以更新Mark Siman的书“ .NET中的依赖注入 ”-主要是他尽快完成了这本书。 在备受尊敬的Dinesh Rajput的编辑中,我们还有一关于Spring 5中设计模式的书,其中的一章还专门介绍了依赖项的实现。

长期以来,我们一直在寻找有趣的材料,这些材料可以回忆起DI范例的优势并阐明我们对它的兴趣-现在已经找到了它。 没错,作者更喜欢在Go中举一些例子。 我们希望这不会阻止您遵循他的想法,并且可以帮助您理解控制反转的一般原理以及使用接口(如果您很接近此主题)。

原稿的情感色彩稍微安静一些,翻译中的感叹号数量减少了。 祝您阅读愉快!

使用接口是一种可以理解的技术,它使您可以创建易于测试且易于扩展的代码。 我一再确信,这是所有功能中最强大的体系结构设计工具。

本文的目的是解释什么是接口,如何使用它们以及它们如何提供代码的可扩展性和可测试性。 最后,本文应展示接口如何帮助优化软件交付管理并简化计划!

介面

该界面描述了合同。 根据语言或框架的不同,可以明确或隐含地指示接口的使用。 因此,在Go语言中, 接口是明确规定的 。 如果尝试将实体用作接口,但与该接口的规则不完全一致,则会发生编译时错误。 例如,执行上面的示例,我们得到以下错误:

prog.go:22:85: cannot use BadPricer literal (type BadPricer) as type StockPricer in argument to isPricerHigherThan100: BadPricer does not implement StockPricer (missing CurrentPrice method) Program exited. 

接口是一种工具,可帮助您将呼叫者与被呼叫者分离,这是通过合同完成的。

让我们使用一个自动交易所交易程序的例子来具体化这个问题。 交易者程序将以设定的购买价格和股票代码被调用。 然后,程序将进入交易所以查找该报价器的当前报价。 此外,如果此报价器的购买价格未超过设定价格,则程序将进行购买。



以简化的形式,该程序的体系结构可以表示如下。 从以上示例中可以明显看出,获取当前价格的操作直接取决于HTTP协议,程序通过该HTTP协议联系交换服务。

Action的状态也直接取决于HTTP。 因此,两个州都应充分了解如何使用HTTP提取交换数据和/或完成交易。

这是实现的样子:

 func analyze(ticker string, maxTradePrice float64) (bool, err) { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil { //   } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ... currentPrice := parsePriceFromBody(body) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err } 

在这里,调用者( analyze )直接依赖于HTTP。 她需要知道如何编写HTTP请求。 他们的解析是如何完成的。 如何处理重试,超时,身份验证等 她对http每当我们调用分析时,也必须调用http

界面如何在这里帮助我们? 在接口提供的协定中,您可以描述行为 ,而不是具体的实现

 type StockExchange interface { CurrentPrice(ticker string) float64 } 

上面定义了StockExchange的概念。 它在此处表示StockExchange支持调用唯一的CurrentPrice函数。 在我看来,这三行代码是最强大的建筑技术。 它们帮助我们更加自信地控制应用程序依赖性。 提供测试。 提供可扩展性。

依赖注入

为了充分理解接口的价值,您需要掌握称为“依赖注入”的技术。

依赖注入意味着调用者提供了调用者需要的东西。 通常看起来像这样:调用方配置对象,然后将其传递给被调用方。 然后,被叫方从配置和实现中抽象出来。 在这种情况下,存在已知的中介。 考虑对HTTP Rest服务的请求。 要实现客户端,我们需要使用可以制定,发送和接收HTTP请求的HTTP库。

如果我们将HTTP请求放置在接口后面,则可以将调用者分离,并且她将“不知道” HTTP请求确实发生了。

调用方应仅进行通用函数调用。 这可以是本地呼叫,远程呼叫,HTTP呼叫,RPC呼叫等。 呼叫者不知道发生了什么,通常只要她能获得预期的结果,它就非常适合她。 下面显示了在我们的analyze方法中依赖注入的外观。

 func analyze(se StockExchange, ticker string, maxTradePrice float64) (bool, error) { currentPrice := se.CurrentPrice(ticker) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err } 

我永远不会对这里发生的事情感到惊讶。 我们完全颠倒了依赖树,并开始更好地控制整个程序。 而且,即使在视觉上,整个实现也变得更加清晰和易于理解。 我们清楚地看到,分析方法应该选择当前价格,检查该价格是否适合我们,如果合适,请进行交易。

最重要的是,在这种情况下,我们将呼叫者与呼叫者分离。 由于调用者和整个实现是使用接口与被调用者分开的,因此可以通过创建接口的许多不同实现来扩展接口。 接口允许您创建许多不同的特定实现,而无需更改被叫方的代码!



该程序中的“获取当前价格”状态仅取决于StockExchange界面。 此实现对如何联系交换服务,如何存储价格或如何发出请求一无所知。 真正的幸福无知。 而且,双边。 HTTPStockExchange实现也对分析一无所知。 关于在何时进行分析的环境,因为挑战是间接发生的。

由于在更改/添加/删除特定实现时不需要更改程序片段(依赖于接口的程序片段), 因此这种设计具有持久性 。 假设我们发现StockService通常不可用。

上面的示例与调用函数有何不同? 在应用函数调用时,实现也将变得更加简洁。 不同之处在于,当您调用该函数时,我们仍然必须求助于HTTP。 analyze方法将仅委派该函数的任务,该任务应调用http ,而不是直接调用http本身。 该技术的全部优势在于“注入”,也就是说,呼叫者将接口提供给被呼叫者。 这正是依赖关系反转发生的方式,其中获取价格仅取决于接口,而不取决于实现。

多种现成的实现

在此阶段,我们具有analyze功能和StockExchange接口,但实际上我们无法做任何有用的事情。 刚刚宣布了我们的计划。 目前,无法调用它,因为我们还没有一个可以满足接口要求的特定实现。

下图中的主要重点是“获取当前价格”状态及其对StockExchange接口的依赖性。 下面显示了两个完全不同的实现如何共存并不清楚当前的价格。 另外,这两种实现方式都不相互关联,它们仅依赖于StockExchange接口。



生产量

原始HTTP实现已存在于主analyze实现中。 我们剩下的就是将其提取并封装在接口的具体实现中。

 type HTTPStockExchange struct {} func (se HTTPStockExchange) CurrentPrice(ticker string) float64 { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil { //   } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) // ... return parsePriceFromBody(body) } 

以前链接到分析功能的代码现在可以自治,并且满足StockExchange接口的要求,也就是说,现在我们可以将其传递给analyze 。 您从上面的图表中回忆起,分析不再与HTTP依赖关系相关联。 使用该界面, analyze不会“想象”幕后发生的事情。 他只知道可以保证给他一个可以调用CurrentPrice的对象。

同样在这里,我们利用封装的典型优点。 以前,当将HTTP请求绑定到分析时,通过HTTP与交换进行通信的唯一方法是间接的-通过analyze方法。 是的,我们可以将这些调用封装到函数中并独立执行该函数,但是接口迫使我们将调用者与调用者分离。 现在,无论调用者如何,我们都可以测试HTTPStockExchange 。 这从根本上影响了我们的测试范围以及我们对测试失败的理解和响应方式。

测试中

在现有代码中,我们具有HTTPStockService结构,该结构使我们可以单独确保它可以与交换服务进行通信并解析从交换服务接收到的响应。 但是,现在让我们确保分析可以正确处理来自StockExchange接口的响应,此外,此操作是可靠且可重复的。

 currentPrice := se.CurrentPrice(ticker) if currentPrice <= maxTradePrice { err := doTrade(ticker, currentPrice) } 

我们可以将实现与HTTP一起使用,但是这样做有很多缺点。 在单元测试中进行网络呼叫可能会很慢,尤其是对于外部服务而言。 由于延迟和不稳定的网络连接,测试结果可能不可靠。 另外,如果我们需要用可以完成交易的语句进行测试,并用可以过滤出不应该结束交易的情况的语句进行测试,那么很难找到可靠地满足这两个条件的实际生产数据条件。 可以选择maxTradePrice ,以这种方式人为地模仿每个条件,例如,使用maxTradePrice := -100交易不应完成,而maxTradePrice := 10000000显然应以交易结束。

但是,如果在交换服务上为我们分配了一定的配额会怎样? 还是我们必须付费访问? 当涉及单元测试时,我们是否真的(并且应该)支付或花费我们的配额? 理想情况下,应尽可能频繁地运行测试,因此它们应该快速,便宜且可靠。 我认为从这一段很明显,为什么在测试方面使用纯HTTP版本是不合理的!

有一个更好的方法,它涉及使用接口!

有了接口,您就可以精心制造StockExchange实现,这将使我们能够快速,安全和可靠地进行analyze

 type StubExchange struct { Price float64 } func (se StubExchange) CurrentPrice(ticker string) float64 { return se.Price } func TestAnalyze_MakeTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 11 traded, err := analyze(se, "TSLA", maxTradePrice) if err != nil { t.Errorf("expected err == nil received: %s", err) } if !traded { t.Error("expected traded == true") } } func TestAnalyze_DontTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 9 traded, err := analyze(se, "TSLA", maxTradePrice) //  } 

上面使用了交换服务的存根,从而启动了我们感兴趣的analyze分支。 然后,在每个测试中进行声明,以确保分析能够满足需要。 尽管这是一个测试程序,但我的经验表明,以近似方式使用接口的组件/体系结构也要通过这种方式进行测试,以确保战斗代码的持久性! 由于有了这些接口,我们可以在内存中使用受控制的StockExchange ,从而提供可靠,易于配置,易于理解,可再现,闪电般的测试!

取消固定-呼叫者配置

既然我们已经讨论了如何使用界面将呼叫者与被呼叫者分离,以及如何执行多种实现,我们仍然没有涉及关键方面。 如何在严格定义的时间配置和提供特定的实现? 您可以直接调用分析函数,但是在生产配置中该怎么做?

这是实现依赖项的方便之处。

 func main() { var ticker = flag.String("ticker", "", "stock ticker symbol to trade for") var maxTradePrice = flag.Float64("maxtradeprice", "", "max price to pay for a share of the ticker symbol." se := HTTPStockExchange{} analyze(se, *ticker, *maxTradePrice) } 

就像在我们的测试案例中一样,将与analyze一起使用的StockExchange的具体具体实现是由analyzer之外的调用者配置的。 然后将其传递(注入)进行analyze 。 这样可以确保在分析NOTHING方面了解如何配置HTTPStockExchange 。 也许我们想以命令行标志的形式提供我们将要使用的http域,然后进行分析就不必更改了。 或者,如果我们需要提供某种身份验证或令牌来访问HTTPStockExchange ,该操作将从环境中提取,该怎么办? 同样,分析不应改变。

配置发生在analyst之外的级别上,从而使分析完全摆脱了配置自己的依赖项的需要。 因此,实现了严格的职责分离。



搁置决定

上面的示例也许已经足够了,但是接口和依赖项注入还有许多其他优点。 接口允许推迟有关特定实现的决策。 尽管决策要求我们决定我们将支持的行为,但是它们仍然允许我们稍后对特定的实现做出决策。 假设我们知道我们想进行自动交易,但是还不确定我们将使用哪个报价提供者。 使用数据仓库时,会不断处理类似的解决方案。 我们的程序应该使用什么:mysql,postgres,redis,文件系统,cassandra? 最终,所有这些都是实现细节,而接口使我们可以推迟就这些问题做出最终决定。 它们使我们能够开发程序的业务逻辑,并在最后时刻切换到特定的技术解决方案!

尽管仅此一项技术就留下了许多可能性,但在项目计划级别还是发生了一些不可思议的事情。 想象一下,如果我们向交换接口再添加一个依赖关系,将会发生什么。



在这里,我们将以有向无环图的形式重新配置我们的体系结构,以便在我们就交换接口的细节达成一致后,就可以使用HTTPStockExchange完全继续使用管道。 我们创造了一种情况,在该情况下,向项目中添加新人员可以帮助我们更快地行动。 通过以这种方式调整体系结构,我们可以更好地了解我们可以在何时何地邀请更多人参与该项目,以加快整个项目的交付速度。 另外,由于接口之间的连接较弱,因此从实现接口开始通常很容易参与工作。 您可以完全独立于我们的程序来开发,测试和测试HTTPStockExchange

对体系结构依存关系的分析和根据这些依存关系进行计划可以极大地加速项目。 使用这种特殊的技术,我能够非常迅速地完成分配了几个月时间的项目。

提前

现在,应该更加清楚接口和依赖项的实现如何确保所设计程序的持久性。 假设我们更改了报价提供者,或者开始流配额并实时保存它们; 您还可以选择其他多种可能性。 当前形式的分析方法将支持任何适合与StockExchange接口集成的实现。

 se.CurrentPrice(ticker) 

因此,在很多情况下,您无需做任何更改即可。 不是全部,而是在我们可能遇到的可预测情况下。 我们不仅免于更改analyze代码并仔细检查其关键功能的需要,而且我们可以轻松提供新的实现方式或在供应商之间切换。 我们还可以平滑扩展或更新我们已经拥有的特定实现,而无需更改或仔细检查analyze

我希望以上示例能令人信服地说明通过使用接口来减弱程序中实体之间的通信,从而完全重新定向依赖关系并使调用者与调用者分离。 由于这种分离,该程序不依赖于特定的实现,而是依赖于特定的行为 。 可以通过多种实现方式提供此行为。 这种关键的设计原理也称为鸭式打字

接口的概念以及对行为的依赖性(而不是对实现的依赖性)是如此强大,以至于我将接口视为语言的原始语言-是的,这非常激进。 我希望上面讨论的示例非常有说服力,并且您将同意从项目一开始就使用接口和依赖项注入。 在我从事的几乎所有项目中,都不需要一个,但是至少需要两个实现:生产和测试。

Source: https://habr.com/ru/post/zh-CN428075/


All Articles