重构遗留代码(8):一个整洁架构的依赖反转

旧代码,丑陋的代码,复杂的代码,意大利面条似的代码,鬼话废话……就是四个字:遗留代码。这是一个系列文章,将有助于你处理并解决它。

是时候谈谈架构和我们如何组织新建立的代码层了。是时候试着将我们的应用程序映射到理论架构设计上了。

 

架构

这是我们已经在文章和教程中到处可见的。整洁架构。

在一个较高级别上,看起来像上面这样的结构,我确信你已经熟悉了。它是Robert C. Martin推荐的架构解决方案。

我们架构的核心是业务逻辑。这些是代表我们的应用程序试图去解决的业务流程的类。这些是代表我们问题域的实体和交互。

接着便是围绕我们业务逻辑的一些其他类型的模块或者类。这些可看做是简单的帮助辅助模块。它们有不同的用途并且它们中的大部分是不可或缺的。它们通过一种传送机制联系了用户和我们应用程序。在我们的例子中,这是个命令行界面。还有另一套辅助类用来连接我们的业务逻辑和表示层及其所有的数据,但在我们的应用程序中没有这样的层。还有像工厂和生产者这样为我们的业务逻辑构造并提供新对象的帮助类。最后,有代表我们系统入口点的类。在我们的例子中,GameRunner可以认为是这样的类,或者我们所有的测试程序也可以以它们各自的方式作为入口点。

图中最值得注意的是依赖方向。所有的辅助类都依赖业务逻辑。业务逻辑不依赖任何其它类。如果我们业务逻辑中的所有对象能神奇地呈现,其包含了所有的数据,无论计算机发生了什么我们都可以直接看见,它们应该能发挥功能。我们的业务逻辑必须能够在没有用户界面或者没有表示层的情况下发挥功能。我们业务逻辑必须独立存在,是在逻辑天地里的一个气泡。

 

A.高级别模块不应依赖低级别模块。两者都应依赖抽象。

B.抽象不应依赖细节。细节应该依赖抽象。

就是它了,最后一个坚实的原则,也许是对你代码有最大作用的那个。它既很容易理解也很容易实现。

简单来说,就是具体的事物应该重视依赖抽象的事物。你的数据库很具体,所有它应该依赖某些更抽象的。你的用户界面很具体,所以它应该依赖某些更抽象的。你的工厂又是很具体的。但你的业务逻辑如何,在你的业务逻辑中你应该持续应用这些观点,以便接近边界的类依赖更加抽象的、更加接近业务逻辑的核心的类。

一个纯粹的业务逻辑,代表了一种抽象的方式,定义域或者业务模型的流程和行为。这样的业务逻辑不含有细节(具体的事物)如值,金钱,账户名,密码,按钮的大小或者表单中的数字域。业务逻辑不应关心具体的事物。它应该只关于你的业务流程。

 

技术诀窍

所以,依赖反转原则(DIP)说的是,无论何时当代码依赖某些具体事物之时,我们应该反转依赖。目前我们的依赖结构像这样。

GameRunner,使用RunnerFunctions.php中的函数创建一个Game类然后使用它。另一方面,Game类代表我们的业务逻辑,创建并使用一个Display对象。

那么,运行者依赖我们的业务逻辑。这是正确的。另一方面,Game类依赖Display类,这就不好了。业务逻辑永远不应该依赖表示层。

我们能用的最简单的技术诀窍是在我们的编程语言中使用抽象结构。一个传统类比一个抽象类更具体,而抽象类比接口更具体。

抽象类是不能被初始化的特殊类型。它只包含定义和部分实现。一个抽象基类通常有多个子类。这些子类从抽象父类继承共有的部分功能,增加它们自己的扩展行为,它们必须实现在抽象父类中定义的所有方法而不是在抽象类中实现。

接口是只允许方法和变量定义的特殊类型。它是面向对象编程中最抽象的结构。任何实现总是必须实现其父类接口的所有方法。一个具体的类可以实现多个接口。

除了C家族的面向对象编程语言,其他像Java或PHP是不允许多继承的。所以一个具体的类可以扩展单一的抽象类但如果需要的话它可以实现多个接口,甚至可以同时实现。或者从另一个角度出发,单一的抽象类可以有许多实现,而许多接口可以有许多实现。

对于DIP一个更全面的解释,请阅读献给这个坚实原则的教程

 

使用接口反转依赖

PHP完全支持接口。从Display类开始作为我们的模型,我们可以定义一个包含对所有类负责的公共方法和有需要实现来显示数据的接口。

看着Display的方法列表,有12个公开的方法,包括了构造函数在内。这是一个相当大的接口,你应该保持这个数字尽可能低,当客户端需要它们时才暴露接口。接口隔离原则关于这方面有些不错的想法。也许我们将在未来的教程中试着处理这个问题。

我们现在想要得到的是类似下面的一个架构。

这种方式,取代了Game类依赖更多Display具体的方法的是,它们都依赖非常抽象的接口。Game类使用接口,而Display类实现它。

 

命名接口

Phil Karlton说,“计算机科学中只有两件难事:缓存失效和命名事物”。

虽然我们不关心缓存,但我们需要命名类,变量和方法。命名接口会是相当大的挑战。

在匈牙利表示法的旧时代,我们可以以这种方式来做。

关于这张图,我们使用了实际的类/文件名和实际的大写。接口以 “Display”签名加上一个字母“I”而命名为“IDisplay”。实际编程语言中需要这样为接口命名。我确信一些读者仍然在使用它们并且此刻笑了。

这种命名模式的问题是错位关注。接口属于它们的客户。我们的接口属于Game类。因此Game类肯定不知道它在用一个接口或一个真实的对象。Game不必关心它真正得到的实现。从Game类的角度来看,它只是用了“Display”,仅此而已。

这解决了Game类调用Display类的命名问题。对实现使用“Impl”后缀比较好些。它有助于消除Game类的关注。

对于我们来说它也更有效。想想Game类现在的样子。它使用一个Display对象并知道怎么使用它。如果我们将接口命名为“Display”,我们将减少Game中需要变更代码的数量。

但尽管如此,这个命名只是略好于前面的那个。它只允许对Display的一个实现,并且实现的名字不能告诉我们提及的是哪种显示。

现在相当好了。我们的实现命名为“CLIDisplay”,如同它输出到CLI。如果我们需要HTML输出或者Windows桌面用户界面,我们可以很容易将其添加到我们的架构中。

因为我们有两种类型的测试程序,慢的金牌大师和快的单元测试,我们想尽可能多得依赖单元测试,尽可能少得依赖金牌大师。所以让我们将金牌大师标记为跳过而试着依赖单元测试。它们现在是通过的,我们想做些修改让其能保持通过状态。但不做上面所有建议的修改,我们怎么做这样的事?

有没有允许我们采取较小步骤的测试方式呢?

 

模拟保存这一天

有这样一种方式。在测试中,这种观念成为“模拟”。

维基百科这样定义模拟,“在面向对象编程中,模拟对象是以控制的方式模仿真实对象行为的对象”。

这样的一个对象将为我们带来很大帮助。事实上,我们甚至不需要复杂得像模拟所有行为的对象。我们需要的是个假的,呆笨的我们能够送入Game类来取代真实显示逻辑的对象。

 

创建接口

让我们创建一个称为Display,包含所有当前具体类所有公开方法的的接口。

正如你看到的,旧的Display.php被重命名为DisplayOld.php了。这只是临时步骤,这允许我们将其排除出去并专注于接口。

这就是创建一个接口。你可以看到它被定义为“interface”而不是“class”。让我们增加方法。

是的。接口只是一堆函数的声明。想象它是C的头文件。没有实现,只有声明。它完全不能包含实现。如果你试着实现任何方法,它将导致错误。但这些非常抽象的定义允许我们做一些很棒的事。我们的Game类现在依赖它们,而不是一个具体的实现。然而,如果我们是试图运行测试程序,它们将失败。

这是因为Gmae类试图在它的构造函数的第25行中创建一个新的display对象。

我们知道不能那么做。接口或者抽象类不能被实例化。我们需要一个真实的对象。

 

依赖注入

在我们的测试中需要一个虚拟对象。一个简单类,实现所有Display接口的方法,但什么也不做。让我们直接把它写在单元测试中。如果你的编程语言不允许在同一文件中有多个类,请自由为你的虚拟类创建一个新文件。

一旦你说你的类实现了接口,IDE将允许你自动填写缺失的方法。这使得创建这样的对象非常快,只要几秒钟时间。

现在让我们通过Game类的构造函数初始化并使用它。

这使得测试通过,但引入了一个巨大的问题。Game类肯定知道了它的测试程序。我们真不想要这样。测试程序只是另一个入口点。DummyDisplay只是另一个用户接口。我们的业务逻辑,Game类,不应该依赖用户界面。所以让我们使其只依赖接口。

但为了测试Game类,我们需要从测试程序送入虚拟的显示。

就是这样。我们需要修改单元测试中的一行。在setup方法中,我们将把DummyDisplay的一个新实例作为参数发送到Game类。这就是依赖注入。使用接口和依赖注入是有帮助的,特别是当你在一个团队工作的时候。我们在Syneto观察到,指定一个类的接口类型并且注入它,将有助于我们与客户端代码的意图更好得沟通。任何看着客户端的人将知道参数中使用的对象类型。并且一个额外的好处是你的IDE将自动完成这些方法,因为它可以确定这些参数的类型。

 

对金牌大师一个真实的实现

金牌大师测试程序,在真实世界运行我们的代码。为了使其通过,我们需要将旧的Display类放入真实的接口实现中,并将其放入我们的业务逻辑。这是这么做的一种方式。

将其重命名为CLIDisplay并使其实现Display。

在RunnerFunctions.php的run()函数中,为CLI创建一个display对象并且当其创建后将其传递给Game类。

取消并运行你的金牌大师测试程序。它们将通过。

 

最后的思考

这种解决方案有效地引向了如下图所示的架构。

所以现在我们的游戏运行者,作为我们应用程序的入口点,创建了一个具体的CLIDisplay类然后依赖它。CLIDisplay类只依赖在表示层和业务逻辑边界上的接口。运行者也直接依赖业务逻辑。这就是我们的应用程序投射到文章开始所用的整洁架构上所呈现的样子。

感谢你的阅读,别错过我们下个谈论详细的模拟和类互动的教程。

收藏 评论

关于作者:EluQ

南京土著,程序员。(新浪微博:@EluQ) 个人主页 · 我的文章 · 11

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部