冷静镇定之争

库图标 三年前,我写了一篇有关Swift语言DI库的文章 。 从那时起,图书馆发生了很大变化,并成为Swinject 同类中最好的竞争对手,在许多方面都超过了它。 本文致力于图书馆的功能,但也有理论上的考虑。 因此,对DI,DIP,IoC主题感兴趣的人,或者在Swinject和Swinject之间做出选择的人,我要求猫:


什么是DIP,IoC?它与什么一起吃?


DIPIoC理论


理论是编程中最重要的组成部分之一。 是的,您无需学习就可以编写代码,但是尽管如此,程序员仍在不断阅读文章,对各种实践感兴趣等。 也就是说,一种或另一种方式是我获得了理论知识以便付诸实践。

人们喜欢要求面试的主题之一是SOLID 。 没有关于他的文章,不要惊慌。 但是我们需要一封信,因为它与我的图书馆紧密相关。 这就是字母“ D”-依赖反转原理。

依赖倒置原则指出:

  • 上级模块不应依赖于下级模块。 两种模块都应依赖抽象。
  • 抽象不应依赖细节。 细节应取决于抽象。

许多人错误地认为,如果他们使用协议/接口,那么他们会自动遵守这一原则,但这并非完全正确。

第一条语句说明了模块之间的依赖关系-模块必须依赖抽象。 等等,什么是抽象? -最好不问自己什么是抽象,而是问自己是什么? 也就是说,您需要了解该过程是什么,并且该过程的结果将是一个抽象。 抽象是在认知过程中从非必要的当事人,属性,关系中分心,以突出必要的常规符号。

根据目标,同一对象可以具有不同的抽象。 例如,从拥有者的角度来看,该机器具有以下重要属性:颜色,优雅,便捷。 但是从机械师的角度来看,一切都有些不同:品牌,型号,改装,里程,参与事故。 刚为一个对象命名了两种不同的抽象-机器。

请注意,在Swift中,习惯上将协议用于抽象,但这不是必需的。 没有人会费心去制作一个类,从中分配一组公共方法,并将实现细节保密。 在抽象方面,没有任何问题。 我们必须记住重要的论点-“抽象与语言无关”-这是一个在我们脑海中不断发生的过程,如何将其转移到代码中并不那么重要。 在这里,我们还可以提及封装 ,作为与语言相关联的示例。 每种语言都有其自己的手段来提供它。 在Swift上,这些是类,访问字段和协议; 在Obj-C接口,协议以及h和m文件的分离上。

第二个语句更有趣,因为它被忽略或误解了。 它讨论了抽象与细节的相互作用,什么是细节? 有一个误解,认为细节是实现协议的类-是的,这是正确的,但并不完整。 您需要了解,细节并不与编程语言绑定-C语言既没有协议也没有类,但是该原理也适用于它。 从理论上讲我很难解释什么是渔获物,因此我将举两个例子,然后尝试证明第二个例子为什么更正确。

假设有一辆汽车和一台发动机。 碰巧我们需要连接它们-机器包含一个引擎。 作为合格的程序员,我们选择协议引擎,实现协议,并将协议的实现传递给机器类。 一切似乎都是正确的-现在,您可以轻松地替换引擎实现,而不用担心会发生故障。 接下来,将引擎机械师添加到电路中。 他对发动机和汽车完全不同的特性感兴趣。 我们正在扩展协议,现在它包含的功能集比起最初的要多。 这个故事对于汽车的拥有者,工厂生产的发动机等等都是重复的。

无反转

但是推理的错误在哪里呢? 问题在于,尽管协议可用,但所描述的连接实际上是“详细信息”-“详细信息”。 更确切地说,引擎以什么名称和协议位于何处。

现在考虑正确的其他选项。

和以前一样,有两个类别-引擎和汽车。 和以前一样,它们必须已连接。 但是现在我们宣布协议“汽车引擎”或“汽车心脏”。 我们仅在其中放置汽车需要引擎提供的那些特性。 而且,我们将协议放置在机器旁边,而不是放置在“引擎”实现旁边。 此外,如果我们需要机械师,我们将需要创建另一个协议并在引擎中实现它。 似乎什么都没有改变,但是方法截然不同-问题不是名称多少,而是协议属于谁以及协议是什么-“抽象”或“详细”。

反转为

现在让我们与另一种情况进行类比,因为这些论点可能并不明显。

有一个后端,需要一些功能。 后端为我们提供了一个包含大量数据的大型方法,并说-“您需要这1000个字段中的这三个字段”

小故事
许多人可以说这不会发生。 而且它们是相对正确的-碰巧后端是为移动应用程序单独编写的。 碰巧,我在一家后端服务是公司的公司工作,该公司具有10年的历史,除其他外,它与State API绑定在一起。 由于许多原因,该公司没有习惯为移动设备编写单独的方法,因此我不得不使用原来的方法。 有一个很棒的方法,它的根中有大约一百个参数,其中一些是嵌套字典。 现在想象一下100个参数,其中20%具有嵌套参数,并且在每个嵌套参数中,还有20-30个具有相同嵌套的参数。 我不完全记得,但是对于简单对象,参数数量超过了800,对于复杂对象,参数数量可能超过了1000。

听起来不是很好,对吧? 通常,后端为前端编写用于特定任务的方法,并且前端是这些方法的客户/用户。 嗯...但是,如果您考虑一下,后端就是引擎,前端就是汽车-机器需要一些引擎特性,而不是需要为引擎赋予汽车特性。 那么,为什么尽管如此,我们仍然继续编写协议引擎,并将其更靠近引擎而不是机器的实现呢? 一切都与规模有关-在大多数iOS程序中,很少需要如此扩展功能以至于这样的解决方案成为问题。

那什么是DI


概念的替代-DI不是DIP的缩写,而是完全不同的缩写,尽管它与DIP非常紧密地相交。 DI是依赖项注入或依赖项注入,而不是反转。 Inversion讨论了类和协议之间应该如何交互,实现说明了从何处获取它们。 通常,您可以通过多种方式实现它-从依赖项开始:构造函数,属性,方法; 最后是创建它们的人以及该过程的自动化程度。 方法不同,但我认为最方便的是依赖注入的容器。 简而言之,它们的全部含义可以归结为一个简单的规则:我们告诉容器在哪里以及如何实现它,然后一切都独立实现。 这种方法对应于“依赖关系的实际实现”-当引入依赖关系的类不知道这种情况如何发生时,即它们是被动的。

在许多语言中,以下方法用于此实现:在单个类/文件中,使用语言语法描述实现规则,然后将其编译并自动实现。 没有魔术-没有自动发生的事情,只是库与语言的基本方法紧密集成在一起,并且使创建方法超载。 因此,对于Swift / Obj-C,通常认为起点是UIViewController,并且库可以轻松地将自己从Storyboard集成到创建的ViewController中。 的确,如果您不使用情节提要,您将不得不用笔做部分工作。

哦,是的,我差点忘了-主要问题的答案:“我们为什么需要这个?” 毫无疑问,您可以自己进行依赖注入,用钢笔开出所有处方。 但是,当图形变大时,就会出现问题-您必须提到类之间的大量连接,代码开始大量增长。 因此,自动递归地(甚至周期性地)实现依赖关系的库会自行处理,并作为奖励来控制它们的生命周期。 也就是说,该库无所不能,它只是简化了开发人员的工作。 没错,不要以为一天就可以写出这样的库-用笔写特定情况下的所有依赖关系是一回事,而教计算机全面,正确地实现则是另一回事。

图书馆历史


如果我不简短讲故事的话,故事是不完整的。 如果您从Beta版本开始使用该库,那么它对您来说就不会那么有趣,但是对于那些第一次看到它的人来说,我认为值得了解它的外观以及作者遵循的目标(即我)。
该库是我的第二个项目,出于自我教育的目的,我决定使用Swift进行编写。 在此之前,我设法编写了一个记录器,但没有将其上传到公共领域-越来越好。

但是,有了DI,这个故事就更有趣了。 当我开始这样做时,我只能在Swift-Swinject上找到一个库。 当时,她有500颗恒星和错误,这些错误和错误通常不会得到处理。 我仔细研究了所有这些……我的行为最好用我最喜欢的短语“然后Ostap受苦”来描述-我浏览了5-6种语言,查看了这些语言的含义,阅读了有关该主题的文章,并意识到可以做得更好。 而现在,将近三年之后,我可以充满信心地说这个目标已经实现,就目前而言,DITranquillity是我眼中最好的。

让我们了解什么是好的DI库:

  • 它应该提供所有基本实现:构造函数,属性,方法
  • 它不应影响业务代码。
  • 她应该清楚地描述出了什么问题。
  • 她必须事先了解哪里有错误,而不是在运行时。
  • 它必须与基本工具(Storyboard)集成在一起
  • 它应该具有简洁的语法。
  • 她必须快速有效地做所有事情。
  • (可选)应该是分层的

我在图书馆的整个开发过程中都遵循这些原则。

图书馆的特色与优势


首先,指向存储库的链接: github.com/ivlevAstef/DITranquillity

对我来说,主要的竞争优势是库谈论启动错误。 启动应用程序并调用所需的功能后,将报告所有存在的和潜在的问题。 这恰恰是库“ calm”名称的含义-实际上,在启动程序之后,该库保证所有必需的依赖项都将存在,并且不存在无法解决的循环。 在有歧义的地方,图书馆会警告可能存在潜在问题。

对我来说听起来不错。 在程序执行期间不会发生崩溃,如果程序员忘记了某些内容,则将立即报告此情况。

我强烈建议使用日志功能来描述问题。 日志记录分为4个级别:错误,警告,信息,详细。 前三个非常重要。 后者不是那么重要-他会写所有发生的事情-哪个对象已注册,哪个对象开始引入,创建了哪个对象,等等。

但这并不是图书馆所拥有的全部:

  • 完全线程安全-可以从任何线程执行任何操作,并且一切正常。 大多数人不需要它,因此在线程安全性方面,已经进行了工作以优化执行速度。 但是,尽管您做出了承诺,但是如果您同时开始注册和接收对象,那么竞争对手库就会崩溃
  • 执行速度快。 在实际设备上,DITranquillity的速度是其竞争对手的两倍。 在模拟器上,执行速度几乎是相等的。 测试链接
  • 体积小-库的重量小于Swinject + SwinjectStoryboad + SwinjectAutoregistration,但是在功能上超过了此捆绑包
  • 简明扼要的音符,虽然会让人上瘾
  • 层次结构。 对于由许多模块组成的大型项目,这是一个很大的优势,因为该库能够根据距当前模块的距离找到必要的类。 也就是说,如果您在每个模块中都有自己的一个协议的实现,那么在每个模块中您将无需付出任何努力即可获得所需的实现。

示范


因此,让我们开始吧。 上次将考虑该项目: SampleHabr 。 我特别没有开始更改示例-因此您可以比较所有更改。 该示例显示了库的许多功能。

为了以防万一,为了避免误解,由于该项目正在展示中,因此它具有许多功能。 但是没有人会以简化的方式使用该库-下载,创建容器,注册几个类,使用该容器。

首先,我们需要创建一个框架(可选):

public class AppFramework: DIFramework { //   public static func load(container: DIContainer) { //     } } 

并在程序开始时,创建您自己的容器,并添加以下框架:

 let container = DIContainer() //   container.append(framework: AppFramework.self) //     . //          ifdef DEBUG      ,         ,     . if !container.validate() { fatalError() } 

故事板


接下来,您需要创建一个基本屏幕。 通常,通常使用Storyboard来实现,在本示例中,我将使用它,但是没有人愿意使用UIViewControllers。

首先,我们需要注册一个情节提要。 为此,请创建一个“部分”(可选-您可以在框架中编写所有代码),并在其中注册了Storyboard:

 import DITranquillity class AppPart: DIPart { static func load(container: DIContainer) { container.registerStoryboard(name: "Main", bundle: nil) .lifetime(.single) //   -    . } } 


并将一部分添加到AppFramework中:
 container.append(part: AppPart.self) 

如您所见,该库具有用于注册Storyboard的便捷语法,我强烈建议您使用它。 原则上,您可以使用此方法编写等效的代码,但是它将更大,并且将无法支持StoryboardReferences。 也就是说,此情节提要板将无法与其他情节提要一起使用。

现在剩下的唯一事情就是创建一个Storyboard并显示开始屏幕。 在检查容器之后,可以在AppDelegate中完成此操作:

 window = UIWindow(frame: UIScreen.main.bounds) ///  Storyboard let storyboard: UIStoryboard = container.resolve(name: "Main") window!.rootViewController = storyboard.instantiateInitialViewController() window!.makeKeyAndVisible() 

使用库创建情节提要不会比平常复杂得多。 在此示例中,由于我们只有一个Storyboard,因此名称可能会被忽略-库将以为您想到了它。 但是在某些项目中,有很多情节提要,因此请不要再错过这个名称。

演示者和ViewController


转到屏幕本身。 我们不会使用复杂的体系结构来加载项目,但是我们将使用通常的MVP。 而且,我很懒惰,我不会为演示者创建协议。 该协议将在另一个类之后发布,这里重要的是要显示如何注册和链接Presenter和ViewController。

为此,将以下代码添加到AppPart中:

 container.register(YourPresenter.init) container.register(YourViewController.self) .injection(\.presenter) //   

这三行代码将允许我们注册两个类,并在它们之间建立连接。

好奇的人可能会好奇-为什么Swinject在一个单独的库中具有的语法成为该项目中的主要语法? 答案在于目标-由于采用了这种语法,该库预先存储了所有链接,而不是在运行时进行计算。 使用此语法,您可以访问许多其他库不可用的功能。

我们启动应用程序,一切正常,创建所有类。

资料


好了,现在我们需要添加一个类和协议来从服务器接收数据:

 public protocol Server { func get(method: String) -> Data? } class ServerImpl: Server { init(domain: String) { ... } func get(method: String) -> Data? { ... } } 

为了美观,我们将为服务器创建一个单独的ServerPart DI类,并在其中注册该类。 让我提醒您,这不是必需的,可以直接在容器中注册,但是我们并不是在寻找简单的方法:)

 import DITranquillity class ServerPart: DIPart { static func load(container: DIContainer) { container.register{ ServerImpl(domain: "https://github.com/") } .as(check: Server.self){$0} .lifetime(.single) } } 

在此代码中,所有内容都不像以前的代码那样透明,需要澄清。 首先,在功能寄存器内部,创建一个带有传递参数的类。

其次,有一个“ as”功能-它表示该类可以通过另一种类型-协议访问。 {$ 0}形式的操作的奇怪结尾是名称`check:`的一部分。 也就是说,此代码确保ServerImpl是Server的后继产品。 但是还有另一种语法:`as(Server.self)`,将执行相同的操作,但不进行检查。 要查看两种情况下编译器将输出什么,可以删除协议实现。

可能有几个“ as”函数-这意味着这些名称中的任何一种都可以使用该类型。 请注意,这将是单次注册,这意味着如果该类是单例,则相同实例可用于任何指定类型。

原则上,如果您想保护自己免于按实现类型创建类的可能性,或者尚未习惯于此语法,则可以编写:

 container.register{ ServerImpl(domain: "https://github.com/") as Server } 

这将是等效的,但不能指定几种单独的类型。

现在您可以在Presenter中实现服务器,为此,我们将修复Presenter,使其可以接受Server:

  class YourPresenter { init(server: Server) { ... } } 

我们启动程序,它落在AppDelegate中的`validate`函数上,并显示一条消息,提示未找到Server类型,但是YourYourPresenter需要它。 怎么了 请注意,该错误发生在程序执行的开始而不是事后。 原因很简单-他们忘记了将ServerPart添加到AppFramework中:

 container.append(part: ServerPart.self) 

我们开始-一切正常。

记录仪


在此之前,人们对机会的了解并不十分深刻,很多机会都有。 现在将演示Swift上的其他库不知道如何做。

在记录器下创建了一个单独的项目

首先,让我们了解什么是记录器。 出于教育目的,我们不会做一个欺骗性的系统,因此记录器是一种具有一种方法和多种实现的协议:

 public protocol Logger { func log(_ msg: String) } class ConsoleLogger: Logger { func log(_ msg: String) { ... } } class FileLogger: Logger { init(file: String) { ... } func log(_ msg: String) { ... } } class ServerLogger: Logger { init(server: String) { ... } func log(_ msg: String) { ... } } class MainLogger: Logger { init(loggers: [Logger]) { ... } func log(_ msg: String) { ... } } 

总计,我们有:

  • 公开协议
  • 3种不同的记录器实现,每个实现都写入不同的位置
  • 一个中央记录器,为其他所有人调用记录功能

该项目创建了LoggerFramework和LoggerPart。 我不会写出他们的代码,但只会写出LoggerPart的内部信息:

 container.register{ ConsoleLogger() } .as(Logger.self) .lifetime(.single) container.register{ FileLogger(file: "file.log") } .as(Logger.self) .lifetime(.single) container.register{ ServerLogger(server: "http://server.com/") } .as(Logger.self) .lifetime(.single) container.register{ MainLogger(loggers: many($0)) } .as(Logger.self) .default() .lifetime(.single) 

我们已经看到了前3个注册,最后一个提出了问题。

参数传递到输入。 创建演示者时已经显示了类似的记录,尽管有一个简短的记录-只是使用了`init`方法,但是没有人愿意这样写:

 container.register { YourPresenter(server: $0) } 

如果有多个参数,则可以使用“ $ 1”,“ $ 2”,“ $ 3”等。 直到16。

但是这个参数调用了“许多”功能。 从这里开始乐趣。 库中有两个修饰符“ many”和“ tag”。
隐藏文字
还有第三个`arg`修饰符,但这并不安全
“许多”修饰符表示您需要获取与所需类型相对应的所有对象。 在这种情况下,期望使用Logger协议,因此将找到并创建从该协议继承的所有类,只有一个例外-本身,即递归地。 尽管在通过属性实现时可以安全地执行此操作,但它不会在初始化期间创建自己。

反过来,标签是必须在使用和注册期间都必须指定的单独的任何类型。 也就是说,如果基本类型不足,则标记是附加条件。

您可以阅读以下内容: 修饰符

修饰符(尤其是“很多”)的存在使该库比其他的更好。 例如,您可以在完全不同的级别上实现Observer模式。 由于这4个字母,在项目中可以从项目中的每个观察者中删除30-50行代码,并解决以下问题:在何时何地将对象添加到Observable中。 清晰的业务不是唯一的应用,而是重要的。

好了,我们将通过在YourPresenter中引入一个记录器来完成功能介绍:

 container.register(YourPresenter.init) .injection { $0.logger = $1 } 

例如,在这里,它的编写方式与以前略有不同-这样做是为了举例说明不同的语法。

请注意,logger属性是可选的:

 internal var logger: Logger? 

而且这不会出现在库的语法中。 与第一个版本不同,现在所有常规类型的操作(可选和强制可选)看起来都相同。 而且,内部逻辑是不同的-如果类型是可选的,并且未在容器中注册,则程序不会崩溃,但会继续执行。

总结


结果与上次相似,只是语法变得更短,更实用。

评论内容:



图书馆还能做什么:



计划


首先,计划在编译阶段切换到检查图形-即与编译器更紧密地集成。 有一个使用SourceKitten的初步实现,但是这种实现在类型推断方面存在严重困难,因此计划切换到ast-dump-迅速在大型项目中使用。 在这里,我要感谢Nekitosss在这方面的巨大贡献。

其次,我想与可视化服务集成。 这将是一个略有不同的项目,但与图书馆密切相关。 有什么意义? 现在,库存储了整个连接图,也就是说,从理论上讲,库中注册的所有内容都可以显示为UML类/组件图。 有时看这张图会很高兴。

该功能分两部分进行了规划-第一部分将允许您添加API以获取所有信息,第二部分已经与各种服务集成。

最简单的选项是以文本形式显示链接图,但我没有看到可读的选项-如果是这样,请在注释中建议选项。

WatchOS-我自己不为他们编写项目。 在他的一生中,他只写过一次,然后写得很少。 但我想与Storyboard紧密集成。

谢谢您的关注。 我真的希望对调查表提出意见和答案。

关于我自己
Ivlev Alexander Evgenievich-iOS团队的高级/团队负责人。 我从事商务工作已经有7年,在iOS 4.5的环境下-在此之前,我是C ++开发人员。 但是总的编程经验超过15年-在学校时,我熟悉了这个奇妙的世界,并被它深深吸引,以至于有一段时间我交换了游戏,食物,厕所和写代码的梦想 。 根据我的一篇文章,您可以猜测我曾经是奥林匹克运动会-因此,用图形编写胜任的工作并不困难。 专业-信息测量系统,有一次我迷上了多线程和并行性-是的,我写了一些代码,对相似的主题进行了假设和错误,但是我理解了问题所在,并且完全理解了可以忽略互斥对象的地方以及在哪里不值得。

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


All Articles