12:依赖关系图¶
在你开始对一个大型项目进行修改之前,你首先需要了解系统是如何工作的,它的类是如何关联的。这一章将帮助你使用一种叫做依赖关系图的工具将其可视化。你会学到
- 什么是依赖关系图?
- 如何用它来理解复杂的系统?
- 如何用它来识别有问题的关系?
- 如何用它把一个复杂的系统分解成模块?
你可以继续使用上一章的项目,或者从本章的启动项目开始。为了获得最好的实践经验,你将需要一支铅笔、红色标记、绿色标记和纸。这将会变得......模拟!
或者,一个绘图程序--甚至是Keynote
--也可以。
开始工作¶
你可能想知道,"究竟什么是依赖关系图?"好问题!
依赖关系图是一种说明类型之间依赖关系的方式。它的主要目的是帮助你理解一个变化将如何影响整个系统。你可以使用依赖关系图来识别变化点、测试点和你可以拉出类型的地方,使你的应用程序更加模块化。
在进行代码修改之前,你的第一步是要确定新的行为应该是什么。在这种情况下,你的工作是将MyBiz
的登录功能移到一个单独的模块中。长期而言,计划是在多个应用程序中使用登录模块。
将登录功能移到一个单独的模块中也有副作用。更快的增量编译时间,单元测试的分离等等。
如果你能简单地将LoginViewController
和相关类型移到一个新的模块中,并让它正常工作,那不是很好吗?不幸的是,现实世界的应用程序通常没有这么好的架构......!
因此,你需要打破依赖关系,使之成为可能。这就是依赖关系图可以帮助你解决的完美问题。
选择开始的地方¶
在一个大型应用中,选择"正确"的地方开始可能是一项艰巨的任务。幸运的是,创建依赖关系图是一个探索的过程,你可以反复地完善它。一个有根据的猜测作为起点已经很好了。
由于这是关于登录的,LoginViewController
是一个好的起点。打开MyBiz.xcodeproj
,从文件层次结构中选择LoginViewController.swift
。
你手边有纸笔吗? (或者绘图程序?)像这样把LoginViewController
写在纸中间的方框内:
就这样,你有了一个起点!
寻找直接依赖关系¶
下一步是确定该类型的直接依赖关系。
当iOS
应用是用Objective-C
编写的时候,这很容易做到:你只需看一下哪些文件被导入。Swift
就比较麻烦了,因为它自动导入同一模块内的类型,而iOS
应用本身就是一个模块。因此,你需要实际滚动一个类型,看看哪些类型被使用,以确定其直接依赖关系。
打开LoginViewController.swift
,你会看到第一行是导入UIKit
。这并不令人惊讶,因为它毕竟是一个UIViewController
。因为系统库很容易在任何地方导入,所以你可以跳过将其添加到你的依赖关系图中。
你会发现下一个依赖来自api
属性,它属于API
类型,可以直接在AppDelegate
上访问。这很有趣,应该被添加到你的图中。请做以下工作:
- 在
LoginViewController
的正上方画一个方框,在其中写上AppDelegate
。 - 从
LoginViewController
框中画一个箭头,指向AppDelegate
框。这表明LoginViewController
对AppDelegate
有依赖性。 - 在
AppDelegate
的右边再画一个框,在里面写上API
。 - 从
AppDelegate
框中画一个箭头,指向API
框。这表明AppDelegate
依赖于API
。即使AppDelegate
没有实际调用API
上的任何方法或属性,它也只是通过对它的引用来依赖它。 - 最后,从
LoginViewController
画一个箭头指向API
,表示它也依赖于它。
下一个依赖对象是Skin
,它是一个用于设计视图的辅助对象。在LoginViewController
的左边添加一个Skin
的框,并在LoginViewController
上画一个箭头指向Skin
。
你的图表现在应该是这样的:
在viewDidLoad
中没有任何依赖性。所以,滚动过它,向下看signIn(_:)
。这个方法很棘手,因为它的依赖关系并没有明确显示。相反,计算属性isEmail
和isValidPassword
定义在Validators.swift
中,showAlert(title:subtitle:type:skin:)
定义在UIViewController+Alert.swift
。
在LoginViewController
的左下方为Validators
画一个新框,在LoginViewController
的正下方为UIViewController+Alert
画一个框。然后,从LoginViewController
画一个箭头指向Validators
,另一个箭头指向UIViewController+Alert
。
你的图表现在应该是这样的:
依赖关系图显示了你的代码是如何相互关联的--类型不需要是类。相反,它们可以是协议、扩展、文件、库或任何其他对你的用例有意义的东西。
同样重要的是要注意,依赖关系图不使用UML
、Archimate
或任何其他正式规范。相反,箭头只表示一种类型依赖于另一种。
继续向下滚动,你会看到LoginViewController
通过扩展与APIDelegate
相一致。在API
的右边画一个方框,在里面写上APIDelegate
。然后,从LoginViewController
画一个箭头,指向APIDelegate
。
在这个扩展中,有几个模型被使用:Event
、Employee
、Announcement
、Product
、PurchaseOrder
和UserInfo
。
你可以选择如何在你的依赖关系图上表示这一点,你有几个选择:
- 为每种类型画一个单独的方框。这样做的好处是可以清楚地表示每一种类型,但会占用更多空间。如果模型有复杂的关系,这是一个不错的选择。例如,对其他类型的依赖,对视图控制器的循环依赖,等等。
- 为模型画一个单一的盒子。这有占用最少空间的优点,但它没有清楚地定义哪些模型被使用。如果模型很简单,没有复杂的关系,而且对你的用例来说,准确显示哪些模型被使用并不重要,那么这是一个很好的选择。
- 为模型画一个单一的框,并在其中列出每个模型。这是上面两个选项之间的权衡。它最小化了空间,但也仍然清楚地定义了哪些模型被使用。如果模型没有复杂的关系,但你仍然想清楚地显示哪些模型被使用,这是一个好的选择。
在这个应用程序中,这些模型没有复杂的关系。然而,LoginViewController
依赖于这么多的模型,这是一种代码的味道,你应该在图中清楚地显示这一点。因此,让我们选择最后一个选项。
在LoginViewController
的左下方再画一个模型框,并在其中列出每个类型。然后,从LoginViewController
画一个箭头,指向Models
。
你的依赖关系图现在应该是这样的:
太棒了! 你已经到达了LoginViewController
的结尾,并且确定了它所有的直接依赖关系。然而,它的依赖关系本身也有依赖关系。这些是LoginViewController
的所谓"二级依赖"。
寻找次要的依赖关系¶
你的依赖关系图现在看起来不错,所有的箭头都指向LoginViewController
。然而,这是因为你只检查了LoginViewController
,还没有检查其他的类。
下一步是确定LoginViewController
的二级依赖关系。这将使你更好地了解对LoginViewController
的改变会对其他类产生怎样的连锁反应。
特别是,LoginViewController
、AppDelegate
和API
之间的半圆形非常可疑,值得进一步调查。
打开AppDelegate.swift
,重复你对LoginViewController
的调查。
你会发现第一个有趣的依赖关系是配置。在AppDelegate
上面为Configuration
画一个新框,并从AppDelegate
画一个箭头指向它。
AppDelegate
也有一个对API
的依赖,但你在前面已经确定了这一点,并且已经在地图上画出了它。
在showLogin
中,你会看到AppDelegate
也有一个对LoginViewController
的依赖关系。画一个箭头,从AppDelegate
指向LoginViewController
来表示这一点。
现在你的图应该是这样的:
啊哦!你发现了一个依赖性循环。AppDelegate
和LoginViewController
是相互依赖的。这可能不会造成现在的问题,但这绝对是一种代码气味,可能会在将来造成问题。你将在下一章中处理这个问题。
关于AppDelegate
的依赖关系就到此为止。那么接下来,打开API.swift
。
这个文件首先定义了APIDelegate
。因为它的方法使用了所有之前确定的模型,所以它依赖于它们。从APIDelegate
到Models
画一个箭头来表明这一点。
即使APIDelegate
与API
被定义在同一个文件中,这实际上并没有使API
依赖于APIDelegate
。如果需要,你可以很容易地将APIDelegate
移到一个单独的文件中。
然而,API
后来声明了一个APIDelegate
类型的delegate
属性,这就对它产生了依赖性。画一个从API
指向APIDelegate
的箭头来显示这一点。
API
还为服务器声明了一个属性,这是一个字符串,它从AppDelegate
上的配置得到。因此,它同时依赖于配置和AppDelegate
。画一个箭头,从API
指向AppDelegate
,另一个箭头指向配置,以显示这一点。
你的依赖关系图现在应该看起来像这样:
哦不!你发现了AppDelegate
和API
之间的另一个循环依赖关系。同样,你将在后面处理这个问题。
API
也有一个新的对Token
的依赖关系。在API
的右上方为Token
画一个新的方框,并从API
画一个箭头指向它。
最后,API
也有一个对URLSession
的依赖关系。然而,这是在Foundation
中定义的。正如你之前对UIKit
的系统依赖一样,你不需要在图中明确指出这一点。
这个类的其余部分没有引入任何新的依赖关系,所以你可以转到你需要检查的下一个文件。UIViewController+Alert.swift
。这个文件声明了对UIViewController
的扩展,它对ErrorViewController
有一个新的依赖关系。
在UIViewController+Alert
下面为ErrorViewController
画一个新的方框,并从UIViewController+Alert
方框中画一个箭头指向ErrorViewController
方框。
现在,你的依赖关系图应该是这样的:
决定何时停止¶
你可以反复浏览所有的文件,为整个应用程序创建一个图表。虽然这可能很有趣,但它很可能太忙而没有用。你离你要修改的类型越远,你就越不可能找到相关的依赖关系。当然,如果你发现自己对不在图上的文件进行了修改,你可以在以后把它们包括进去。
作为一个理智的检查,以验证你已经走得够远了,对LoginViewController
进行文本搜索,看看是否有其他文件引用它。你会发现ErrorViewController
实际上有一个对LoginViewController
的引用。
打开ErrorViewController
,你会看到它在secondaryAction(_:)
中使用了LoginViewController
。添加一个箭头,从ErrorViewController
指向LoginViewController
来表示。
这揭示了LoginViewController
、UIViewController+Alert
和ErrorViewController
之间的间接循环。
ErrorViewController
也有一个Skin
类型的属性。从ErrorViewController
添加一个箭头,指向Skin
。
最终,你的图应该是这样的:
什么是有问题的依赖关系?¶
当一个类型直接依赖于另一个类型时,它就被耦合到了另一个类型。然而,这可能是有问题的,也可能不是。例如,如果一个类型被耦合到一个委托协议(如API
和APIDelegate
),这比直接耦合到一个具体类型(如LoginViewController
)要好。
紧密耦合指的是一种"有问题"的依赖关系,不能轻易换掉。这就提出了一个问题。什么是有问题的依赖关系?简单地说,如果一个依赖关系阻碍了你完成你的目标,那么它就是有问题的。
在这个例子中,你的目标是把登录拉到一个单独的模块中。任何阻碍这一点的东西都是有问题的依赖关系。
实际上,你怎样才能识别这些有问题的依赖关系呢?请对LoginViewController
的每个直接依赖关系提出以下问题:
- 对
AppDelegate
的依赖性吗?根据定义,AppDelegate
代表应用程序,所以它不能被拉入模块。因此,这将是有问题的。 - 依赖关系是循环的吗?如果是的话,你可能需要打破一方或双方的关系。
- 该依赖关系是否有许多次级依赖关系?如果是这样,就很难把它拉到模块中。
- 依赖关系被拉入同一模块是否有意义?即使有可能将该依赖拉入同一模块,也可能不适合这样做。
创建另一个模块可能是有意义的,但你应该仔细计划如何做才是最好的。如果该依赖关系在整个应用中的许多地方都被使用,这一点尤其正确。
寻找有问题的依赖关系¶
你可以使用这些问题来评估依赖关系图中的关系,以找到有问题的依赖关系。
首先,AppDelegate
上是否有依赖关系?是的,有。有问题的关系是指指向AppDelegate
的箭头。这包括LoginViewController
对AppDelegate
的依赖,以及API
对AppDelegate
的依赖。
你的红色标记在手边吗?把这两个箭头都用红色标注出来,表示它们是有问题的。
你永远不能把AppDelegate
拉到一个模块中,所以一般来说它是有问题的。突出显示AppDelegate
框的红色,以表明这一点。
有任何循环依赖关系吗?是的,也有。LoginViewController
依赖于API
,它从AppDelegate
获得。反过来,AppDelegate
又依赖于LoginViewController
。
你已经发现LoginViewController
与AppDelegate
之间的关系是有问题的。那AppDelegate
到LoginViewController
的关系呢?这是否会阻止你把登录拉到一个单独的模块中?
不,实际上。AppDelegate
可以依赖新的登录模块,反过来,它仍然可以设置LoginViewController
。因此,就你的目标而言,这并没有什么问题。
那LoginViewController
与API
的关系呢?是的,这是个问题,有两个原因:
API
在整个应用程序的其他地方使用,所以很难将其拉入登录模块。API
在概念上对登录模块没有意义。它知道所有的模型和应用程序中的网络调用。这已经远远超出了login
应该知道的范围。
因此,突出LoginViewController
到API
的箭头和红色的API
框,表明这些是有问题的。
LoginViewController
是否有许多次要的依赖关系?是的,APIDelegate
依赖于很多模型。
登录模块真的需要知道这些模型中的任何一个吗?与登录有关的两个APIDelegate
方法是loginFailed(error:)
和loginSucceeded(userId:)
。这两个方法实际上都没有使用这些模型!
因此,LoginViewController
到APIDelegate
的关系,LoginViewController
到模型的关系以及APIDelegate
盒子本身都有问题。把这些都用红色标注出来。
你的图表现在应该是这样的:
LoginViewController
还有三个直接依赖关系:Skin
,Validators
和UIViewController+Alert
。把这些拉到login
的同一个模块里有意义吗?
如果Skin
只被LoginViewController
使用,那么把它拉到同一个模块里也许是可以的。然而,它也被ErrorViewController
使用,所以这样做是不对的。高亮显示LoginViewController
到Skin
的关系和Skin
框本身的红色。
Validators
应该被移到同一个登录模块中吗?是的,实际上! 它只被LoginViewController
使用,而且它的方法明确地与登录验证有关。突出这种关系,Validators
框用绿色表示可以移动。
UIViewController+Alert
在同一个登录模块中,有意义吗?不是的,它是一个通用的组件,在整个应用程序的多个地方使用。实际上,它本身放在一个单独的模块中可能有意义,但它不属于登录模块。因此,将LoginViewController
到UIViewController+Alert
的关系和UIViewController+Alert
框本身用红色标出。
最终,你的依赖关系图应该是这样的:
完成映射¶
如果你发现一个直接依赖关系有问题,你不需要评估它的次级依赖关系是否有问题。相反,你需要先以某种方式重构或修复这个问题。不过,根据你在这方面的做法,你可能会在以后考虑次级依赖关系,也可能永远不会这样做。
如果你发现一个依赖关系没有问题,你确实需要评估它的次级依赖关系。可能会发现一些次要的依赖关系是有问题的,你需要以某种方式添加它们。
一旦你完成了对所有相关依赖关系的评估,你就完成了对映射上依赖关系的评估
在这种情况下,你实际上已经完成了这两项工作,所以你的映射是好的。
打破复杂的系统¶
你可以把你的依赖关系图作为一个蓝图来分解复杂的系统。它准确地告诉你各种类型是如何关联的,哪些关系是有问题的!
当然,还有一个问题,就是实际处理有问题的关系。你通常不能简单地删除一个关系,因为它在提供某种有用的功能。
那么实际上,你怎么能解决这些问题呢?当然是使用TDD
! 是的,这比简单的"神奇的TDD
代码"更有意义,但你会在下一章中了解到这一切!
关键点¶
你在本章中了解了依赖关系图。下面是它们的要点。
- 依赖关系图是一种将代码依赖关系可视化的工具。
- 你可以用它们来发现有问题的关系。
- 你可以把它们作为分解复杂系统的蓝图。
从这里开始,要去哪里?¶
在下一章中,你将使用这个依赖关系图,将登录功能实际拉出到一个新的模块中!当然,你会这样做。当然,你会以TDD
的方式来做这件事,并在这个过程中学习处理问题关系的技巧。
继续看下一章,了解所有的内容!