跳转至

12:依赖关系图

在你开始对一个大型项目进行修改之前,你首先需要了解系统是如何工作的,它的类是如何关联的。这一章将帮助你使用一种叫做依赖关系图的工具将其可视化。你会学到

  • 什么是依赖关系图?
  • 如何用它来理解复杂的系统?
  • 如何用它来识别有问题的关系?
  • 如何用它把一个复杂的系统分解成模块?

你可以继续使用上一章的项目,或者从本章的启动项目开始。为了获得最好的实践经验,你将需要一支铅笔、红色标记、绿色标记和纸。这将会变得......模拟!

或者,一个绘图程序--甚至是Keynote--也可以。

开始工作

你可能想知道,"究竟什么是依赖关系图?"好问题!

依赖关系图是一种说明类型之间依赖关系的方式。它的主要目的是帮助你理解一个变化将如何影响整个系统。你可以使用依赖关系图来识别变化点、测试点和你可以拉出类型的地方,使你的应用程序更加模块化。

在进行代码修改之前,你的第一步是要确定新的行为应该是什么。在这种情况下,你的工作是将MyBiz的登录功能移到一个单独的模块中。长期而言,计划是在多个应用程序中使用登录模块。

将登录功能移到一个单独的模块中也有副作用。更快的增量编译时间,单元测试的分离等等。

如果你能简单地将LoginViewController和相关类型移到一个新的模块中,并让它正常工作,那不是很好吗?不幸的是,现实世界的应用程序通常没有这么好的架构......!

因此,你需要打破依赖关系,使之成为可能。这就是依赖关系图可以帮助你解决的完美问题。

选择开始的地方

在一个大型应用中,选择"正确"的地方开始可能是一项艰巨的任务。幸运的是,创建依赖关系图是一个探索的过程,你可以反复地完善它。一个有根据的猜测作为起点已经很好了。

由于这是关于登录的,LoginViewController是一个好的起点。打开MyBiz.xcodeproj,从文件层次结构中选择LoginViewController.swift

你手边有纸笔吗? (或者绘图程序?)像这样把LoginViewController写在纸中间的方框内:

image

就这样,你有了一个起点!

寻找直接依赖关系

下一步是确定该类型的直接依赖关系。

iOS应用是用Objective-C编写的时候,这很容易做到:你只需看一下哪些文件被导入。Swift就比较麻烦了,因为它自动导入同一模块内的类型,而iOS应用本身就是一个模块。因此,你需要实际滚动一个类型,看看哪些类型被使用,以确定其直接依赖关系。

打开LoginViewController.swift,你会看到第一行是导入UIKit。这并不令人惊讶,因为它毕竟是一个UIViewController。因为系统库很容易在任何地方导入,所以你可以跳过将其添加到你的依赖关系图中。

你会发现下一个依赖来自api属性,它属于API类型,可以直接在AppDelegate上访问。这很有趣,应该被添加到你的图中。请做以下工作:

  1. LoginViewController的正上方画一个方框,在其中写上AppDelegate
  2. LoginViewController框中画一个箭头,指向AppDelegate框。这表明LoginViewControllerAppDelegate有依赖性。
  3. AppDelegate的右边再画一个框,在里面写上API
  4. AppDelegate框中画一个箭头,指向API框。这表明AppDelegate依赖于API。即使AppDelegate没有实际调用API上的任何方法或属性,它也只是通过对它的引用来依赖它。
  5. 最后,从LoginViewController画一个箭头指向API,表示它也依赖于它。

下一个依赖对象是Skin,它是一个用于设计视图的辅助对象。在LoginViewController的左边添加一个Skin的框,并在LoginViewController上画一个箭头指向Skin

你的图表现在应该是这样的:

image

viewDidLoad中没有任何依赖性。所以,滚动过它,向下看signIn(_:)。这个方法很棘手,因为它的依赖关系并没有明确显示。相反,计算属性isEmailisValidPassword定义在Validators.swift中,showAlert(title:subtitle:type:skin:)定义在UIViewController+Alert.swift

LoginViewController的左下方为Validators画一个新框,在LoginViewController的正下方为UIViewController+Alert画一个框。然后,从LoginViewController画一个箭头指向Validators,另一个箭头指向UIViewController+Alert

你的图表现在应该是这样的:

image

依赖关系图显示了你的代码是如何相互关联的--类型不需要是类。相反,它们可以是协议、扩展、文件、库或任何其他对你的用例有意义的东西。

同样重要的是要注意,依赖关系图不使用UMLArchimate或任何其他正式规范。相反,箭头只表示一种类型依赖于另一种。

继续向下滚动,你会看到LoginViewController通过扩展与APIDelegate相一致。在API的右边画一个方框,在里面写上APIDelegate。然后,从LoginViewController画一个箭头,指向APIDelegate

在这个扩展中,有几个模型被使用:EventEmployeeAnnouncementProductPurchaseOrderUserInfo

你可以选择如何在你的依赖关系图上表示这一点,你有几个选择:

  1. 为每种类型画一个单独的方框。这样做的好处是可以清楚地表示每一种类型,但会占用更多空间。如果模型有复杂的关系,这是一个不错的选择。例如,对其他类型的依赖,对视图控制器的循环依赖,等等。
  2. 为模型画一个单一的盒子。这有占用最少空间的优点,但它没有清楚地定义哪些模型被使用。如果模型很简单,没有复杂的关系,而且对你的用例来说,准确显示哪些模型被使用并不重要,那么这是一个很好的选择。
  3. 为模型画一个单一的框,并在其中列出每个模型。这是上面两个选项之间的权衡。它最小化了空间,但也仍然清楚地定义了哪些模型被使用。如果模型没有复杂的关系,但你仍然想清楚地显示哪些模型被使用,这是一个好的选择。

在这个应用程序中,这些模型没有复杂的关系。然而,LoginViewController依赖于这么多的模型,这是一种代码的味道,你应该在图中清楚地显示这一点。因此,让我们选择最后一个选项。

LoginViewController的左下方再画一个模型框,并在其中列出每个类型。然后,从LoginViewController画一个箭头,指向Models

你的依赖关系图现在应该是这样的:

image

太棒了! 你已经到达了LoginViewController的结尾,并且确定了它所有的直接依赖关系。然而,它的依赖关系本身也有依赖关系。这些是LoginViewController的所谓"二级依赖"。

寻找次要的依赖关系

你的依赖关系图现在看起来不错,所有的箭头都指向LoginViewController。然而,这是因为你只检查了LoginViewController,还没有检查其他的类。

下一步是确定LoginViewController的二级依赖关系。这将使你更好地了解对LoginViewController的改变会对其他类产生怎样的连锁反应。

特别是,LoginViewControllerAppDelegateAPI之间的半圆形非常可疑,值得进一步调查。

打开AppDelegate.swift,重复你对LoginViewController的调查。

你会发现第一个有趣的依赖关系是配置。在AppDelegate上面为Configuration画一个新框,并从AppDelegate画一个箭头指向它。

AppDelegate也有一个对API的依赖,但你在前面已经确定了这一点,并且已经在地图上画出了它。

showLogin中,你会看到AppDelegate也有一个对LoginViewController的依赖关系。画一个箭头,从AppDelegate指向LoginViewController来表示这一点。

现在你的图应该是这样的:

image

啊哦!你发现了一个依赖性循环。AppDelegateLoginViewController是相互依赖的。这可能不会造成现在的问题,但这绝对是一种代码气味,可能会在将来造成问题。你将在下一章中处理这个问题。

关于AppDelegate的依赖关系就到此为止。那么接下来,打开API.swift

这个文件首先定义了APIDelegate。因为它的方法使用了所有之前确定的模型,所以它依赖于它们。从APIDelegateModels画一个箭头来表明这一点。

即使APIDelegateAPI被定义在同一个文件中,这实际上并没有使API依赖于APIDelegate。如果需要,你可以很容易地将APIDelegate移到一个单独的文件中。

然而,API后来声明了一个APIDelegate类型的delegate属性,这就对它产生了依赖性。画一个从API指向APIDelegate的箭头来显示这一点。

API还为服务器声明了一个属性,这是一个字符串,它从AppDelegate上的配置得到。因此,它同时依赖于配置和AppDelegate。画一个箭头,从API指向AppDelegate,另一个箭头指向配置,以显示这一点。

你的依赖关系图现在应该看起来像这样:

image

哦不!你发现了AppDelegateAPI之间的另一个循环依赖关系。同样,你将在后面处理这个问题。

API也有一个新的对Token的依赖关系。在API的右上方为Token画一个新的方框,并从API画一个箭头指向它。

最后,API也有一个对URLSession的依赖关系。然而,这是在Foundation中定义的。正如你之前对UIKit的系统依赖一样,你不需要在图中明确指出这一点。

这个类的其余部分没有引入任何新的依赖关系,所以你可以转到你需要检查的下一个文件。UIViewController+Alert.swift。这个文件声明了对UIViewController的扩展,它对ErrorViewController有一个新的依赖关系。

UIViewController+Alert下面为ErrorViewController画一个新的方框,并从UIViewController+Alert方框中画一个箭头指向ErrorViewController方框。

现在,你的依赖关系图应该是这样的:

image

决定何时停止

你可以反复浏览所有的文件,为整个应用程序创建一个图表。虽然这可能很有趣,但它很可能太忙而没有用。你离你要修改的类型越远,你就越不可能找到相关的依赖关系。当然,如果你发现自己对不在图上的文件进行了修改,你可以在以后把它们包括进去。

作为一个理智的检查,以验证你已经走得够远了,对LoginViewController进行文本搜索,看看是否有其他文件引用它。你会发现ErrorViewController实际上有一个对LoginViewController的引用。

打开ErrorViewController,你会看到它在secondaryAction(_:)中使用了LoginViewController。添加一个箭头,从ErrorViewController指向LoginViewController来表示。

这揭示了LoginViewControllerUIViewController+AlertErrorViewController之间的间接循环。

ErrorViewController也有一个Skin类型的属性。从ErrorViewController添加一个箭头,指向Skin

最终,你的图应该是这样的:

image

什么是有问题的依赖关系?

当一个类型直接依赖于另一个类型时,它就被耦合到了另一个类型。然而,这可能是有问题的,也可能不是。例如,如果一个类型被耦合到一个委托协议(如APIAPIDelegate),这比直接耦合到一个具体类型(如LoginViewController)要好。

紧密耦合指的是一种"有问题"的依赖关系,不能轻易换掉。这就提出了一个问题。什么是有问题的依赖关系?简单地说,如果一个依赖关系阻碍了你完成你的目标,那么它就是有问题的。

在这个例子中,你的目标是把登录拉到一个单独的模块中。任何阻碍这一点的东西都是有问题的依赖关系。

实际上,你怎样才能识别这些有问题的依赖关系呢?请对LoginViewController的每个直接依赖关系提出以下问题:

  1. AppDelegate的依赖性吗?根据定义,AppDelegate代表应用程序,所以它不能被拉入模块。因此,这将是有问题的。
  2. 依赖关系是循环的吗?如果是的话,你可能需要打破一方或双方的关系。
  3. 该依赖关系是否有许多次级依赖关系?如果是这样,就很难把它拉到模块中。
  4. 依赖关系被拉入同一模块是否有意义?即使有可能将该依赖拉入同一模块,也可能不适合这样做。

创建另一个模块可能是有意义的,但你应该仔细计划如何做才是最好的。如果该依赖关系在整个应用中的许多地方都被使用,这一点尤其正确。

寻找有问题的依赖关系

你可以使用这些问题来评估依赖关系图中的关系,以找到有问题的依赖关系。

首先,AppDelegate上是否有依赖关系?是的,有。有问题的关系是指指向AppDelegate的箭头。这包括LoginViewControllerAppDelegate的依赖,以及APIAppDelegate的依赖。

你的红色标记在手边吗?把这两个箭头都用红色标注出来,表示它们是有问题的。

你永远不能把AppDelegate拉到一个模块中,所以一般来说它是有问题的。突出显示AppDelegate框的红色,以表明这一点。

有任何循环依赖关系吗?是的,也有。LoginViewController依赖于API,它从AppDelegate获得。反过来,AppDelegate又依赖于LoginViewController

你已经发现LoginViewControllerAppDelegate之间的关系是有问题的。那AppDelegateLoginViewController的关系呢?这是否会阻止你把登录拉到一个单独的模块中?

不,实际上。AppDelegate可以依赖新的登录模块,反过来,它仍然可以设置LoginViewController。因此,就你的目标而言,这并没有什么问题。

LoginViewControllerAPI的关系呢?是的,这是个问题,有两个原因:

  1. API在整个应用程序的其他地方使用,所以很难将其拉入登录模块。
  2. API在概念上对登录模块没有意义。它知道所有的模型和应用程序中的网络调用。这已经远远超出了login应该知道的范围。

因此,突出LoginViewControllerAPI的箭头和红色的API框,表明这些是有问题的。

LoginViewController是否有许多次要的依赖关系?是的,APIDelegate依赖于很多模型。

登录模块真的需要知道这些模型中的任何一个吗?与登录有关的两个APIDelegate方法是loginFailed(error:)loginSucceeded(userId:)。这两个方法实际上都没有使用这些模型!

因此,LoginViewControllerAPIDelegate的关系,LoginViewController到模型的关系以及APIDelegate盒子本身都有问题。把这些都用红色标注出来。

你的图表现在应该是这样的:

image

LoginViewController还有三个直接依赖关系:SkinValidatorsUIViewController+Alert。把这些拉到login的同一个模块里有意义吗?

如果Skin只被LoginViewController使用,那么把它拉到同一个模块里也许是可以的。然而,它也被ErrorViewController使用,所以这样做是不对的。高亮显示LoginViewControllerSkin的关系和Skin框本身的红色。

Validators应该被移到同一个登录模块中吗?是的,实际上! 它只被LoginViewController使用,而且它的方法明确地与登录验证有关。突出这种关系,Validators框用绿色表示可以移动。

UIViewController+Alert在同一个登录模块中,有意义吗?不是的,它是一个通用的组件,在整个应用程序的多个地方使用。实际上,它本身放在一个单独的模块中可能有意义,但它不属于登录模块。因此,将LoginViewControllerUIViewController+Alert的关系和UIViewController+Alert框本身用红色标出。

最终,你的依赖关系图应该是这样的:

image

完成映射

如果你发现一个直接依赖关系有问题,你不需要评估它的次级依赖关系是否有问题。相反,你需要先以某种方式重构或修复这个问题。不过,根据你在这方面的做法,你可能会在以后考虑次级依赖关系,也可能永远不会这样做。

如果你发现一个依赖关系没有问题,你确实需要评估它的次级依赖关系。可能会发现一些次要的依赖关系是有问题的,你需要以某种方式添加它们。

一旦你完成了对所有相关依赖关系的评估,你就完成了对映射上依赖关系的评估

在这种情况下,你实际上已经完成了这两项工作,所以你的映射是好的。

打破复杂的系统

你可以把你的依赖关系图作为一个蓝图来分解复杂的系统。它准确地告诉你各种类型是如何关联的,哪些关系是有问题的!

当然,还有一个问题,就是实际处理有问题的关系。你通常不能简单地删除一个关系,因为它在提供某种有用的功能。

那么实际上,你怎么能解决这些问题呢?当然是使用TDD! 是的,这比简单的"神奇的TDD代码"更有意义,但你会在下一章中了解到这一切!

关键点

你在本章中了解了依赖关系图。下面是它们的要点。

  • 依赖关系图是一种将代码依赖关系可视化的工具。
  • 你可以用它们来发现有问题的关系。
  • 你可以把它们作为分解复杂系统的蓝图。

从这里开始,要去哪里?

在下一章中,你将使用这个依赖关系图,将登录功能实际拉出到一个新的模块中!当然,你会这样做。当然,你会以TDD的方式来做这件事,并在这个过程中学习处理问题关系的技巧。

继续看下一章,了解所有的内容!