当前位置: 首页 > 科技观察

依赖注入及其在抖音直播中的应用

时间:2023-03-15 09:37:53 科技观察

作者|周武坤前言在过去的三年里,抖音直播业务实现了爆发式增长,直播间的功能也增加了不少可玩性。为了高效满足业务快速迭代的需求,抖音直播非常深入地使用了依赖注入架构。在软件工程中,依赖注入的意思是:给调用者它需要的东西。“依赖性”是可以由方法调用的东西。在依赖注入的形式下,调用者不再直接使用“依赖”,而是“注入”。“注入”是指将“依赖项”传递给调用者的过程。“注入”之后,调用者会调用“依赖”。将依赖传递给调用者而不是让调用者直接获取依赖是设计的基本要求。这样设计的目的是将调用方和依赖方分离,从而实现代码的高内聚和低耦合,提高可读性和复用性。本文试图从原理入手,阐明什么是依赖,什么是反转,依赖反转和控制反转是什么关系?依赖注入框架应该具备哪些能力?抖音直播是如何通过依赖注入优雅的实现模块之间的解耦的?通过对依赖注入架构优缺点的分析,可以让我们对它有一个更全面的认识,为后续的架构设计工作带来更多的启发。什么是依赖对象面向对象设计和编程的基本思想是把一个复杂的系统分解成相互协作的对象。这些对象类被封装后,内部实现对外是透明的,从而减少了解决问题的过程。复杂性,服务可以灵活复用和扩展。面向对象设计带来的最直接的问题就是对象之间的依赖关系。举一个开发中最常见的例子:如果在A类中使用了B类的实例化结构,那么可以说A依赖于B。在软件系统引入IOC容器之前,对象A依赖于对象B,那么当对象A初始化或运行到一定程度时,它必须主动创建对象B或使用创建的对象B。无论对象B是创建还是使用,控制权都在A自己手中。这种直接依赖会导致什么问题?过渡性暴露细节A只关心B提供的接口服务,不关心B内部的实现细节,A因为依赖引入了B类,间接关心B的实现细节,强耦合的任何变化B中的对象之间会影响A,开发A和B的人不一定是同一个人。B改变了A需要使用的一个方法参数。B的修改可以编译继续使用,但是A不能运行。可扩展性很差。A是服务使用者,B是提供特定的服务,如果C也能提供类似的服务,但是A已经严重依赖B,很难用C替代。学过面向对象的同学马上就会知道他们可以使用接口来解决上述问题。问题。如果在早期实现B类的时候定义了一个接口,B和C都实现了这个接口中的方法,那么从B到C的切换只需稍作改动即可。A对B或C的依赖变成了对抽象接口的依赖,上述问题都解决了。但是目前我们还是要实例化B或者C,因为new只能创建新的对象,不能创建接口,不能说A完全依赖接口。从B切换到C还需要修改代码,能不能少点依赖?能不能在A运行的时候想切换到B就切换到B,想切换到C就切换到C?以后不改代码还能支持转D吗?通过反思,上述诉求很容易实现。比如常用的接口NSClassFromString,可以通过字符串转换成同名的类。通过读取本地的配置文件或者服务端下发的数据,通过OC提供的反射接口获取对应的类,可以在运行时动态控制依赖对象的引入。软件系统的依赖可以让我们把视角放到更大的软件系统中,这种依赖问题会更加突出。在面向对象设计的软件系统中,其底层通常由N个对象组成,各个对象或模块相互协作,最终实现系统的业务逻辑。如果我们打开机械表的后盖,我们会看到类似上述的情况。每个齿轮带动时针、分针和秒针顺时针转动,从而在表盘上产生正确的时间。上图描述了这样一个齿轮组,它有多个相互啮合的独立齿轮,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果其中一个齿轮出现问题,可能会影响到整个齿轮组的正常运转。齿轮组中齿轮之间的啮合关系非常类似于软件系统中对象之间的耦合关系。对象之间的耦合关系是不可避免的,也是必要的,是协同工作的基础。对于功能越复杂的应用程序,对象之间的依赖关系一般越复杂,经常会出现对象之间的多重依赖关系。因此,架构师在系统分析和设计方面将面临更大的挑战。一个对象间耦合度太高的系统,必然会牵一发而动全身。耦合关系不仅会出现在对象之间,还会出现在软件系统的模块之间。如何降低系统、模块和对象之间的耦合度是软件工程一直追求的目标之一。控制反转为了解决对象之间过度耦合的问题,软件专家MichaelMattson于1996年提出了IOC理论,以实现对象之间的“解耦”。目前,该理论已成功应用于实践。1996年MichaelMattson在一篇关于面向对象框架的文章中首次提出了IOC(InversionofControl/控制反转)的概念。IOC理论提出的观点大体上是:借助于“第三方”来实现具有依赖关系的对象之间的解耦。如下图所示:由于在中间位置引入了“第三方”,即IOC容器,使得A、B、C、D这四个对象没有耦合关系,它们之间的传递齿轮都取决于“第三方”。对象的控制权全部交给“第三方”IOC容器。因此,IOC容器成为了整个系统的关键核心。它充当“胶水”,将系统中的所有对象粘合在一起。发挥作用,没有这种“胶水”,对象之间就会失去联系,这也是为什么有人把IOC容器比作“胶水”的原因。我们再做一个实验:把上图中间的IOC容器去掉,再看看这个系统:我们现在看到的这个图就是我们要实现整个系统所需要完成的。此时,A、B、C、D四个对象之间不存在耦合关系,彼此之间没有任何关系。这样的话,当你实现A的时候,根本不需要考虑B、C、D。对象之间的依赖性已降至最低。因此,如果IOC容器能够实现,对于系统开发来说将是一件多么美妙的事情。每个参与开发的成员只需要实现自己的类即可,与其他人无关!软件系统引入IOC容器后,彻底改变了对象之间的依赖关系。由于加入了IOC容器,对象A和对象B之间的直接联系就失去了。因此,当对象A运行到需要对象B的地方时,IOC容器会主动创建一个对象B,注入到对象A需要它的地方。通过前后对比,我们不难看出:对象A获取依赖对象B的过程,由主动行为变为被动行为,控制权发生了逆转。这就是“InversionofControl”这个名字的由来。依赖倒置和没有倒置的控制倒置当我们考虑如何解决一个高层次的问题时,我们会把它拆解成一系列更详细的低层次问题,然后每个更下层的问题拆解成一系列更下层的问题,这是业务逻辑(控制流)的方向,是一种“自上而下”的设计。如果我们按照这种拆解的思路来组织我们的代码问题,那么代码架构的方向和业务逻辑的方向是一致的,即没有反转,没有依赖反转,系统行为决定控制流,控制流决定代码依赖。抖音直播为例:直播有房间的概念,房间包含多个功能组件。对应的,代码中包含一个房间服务控制器类(如RoomController),一个组件ma管理类(ComponentLoader),以及几个组件类(比如红包组件RedEnvelopeComponent,礼物组件GiftComponent)。进入直播间时,首先创建一个房间控制器,控制器会创建一个组件管理类,然后组件管理类会初始化房间内的所有组件。这里描述的是业务逻辑(控制流)的方向。如果按照不反转的情况,控制流和代码依赖示意图如下:不反转的伪代码示例如下:@implementationRoomController-(void)viewDidLoad{//初始化房间服务self.componentLoader=[[ComponentLoader分配]初始化];[self.componentLoadersetupComponents];}@end@implementationComponentLoader-(void)setupComponents{//初始化所有房间组件ComponentA*a=[[ComponentAalloc]init];ComponentB*b=[[ComponentBalloc]init];ComponentC*c=[[ComponentCalloc]init];自己。组件=@[a,b,c];[设置];[b设置];[csetup];}@end@implementationComponentA-(void)setup{}@end@implementationComponentB-(void)setup{}@end@implementationComponentC-(void)setup{}@end依赖倒置(DIP)一SOLID原则之一:DIP(依赖倒置原则)。这里的依赖是指代码层面的依赖。上层模块不应该依赖下层模块,它们都应该依赖抽象(上层模块定义和依赖抽象接口,下层模块实现接口)。反转是指:反转源代码的依赖方向,使其与控制流的方向相反。依赖倒置代码示例如下:@protocolComponentInterface-(void)setup;@end@interfaceComponentA@end@interfaceComponentB@end@interfaceComponentC@end@implementationComponentLoader-(void)setModules{//初始化组件ComponentA*a=[[ComponentAalloc]init];ComponentB*b=[[ComponentBalloc]init];ComponentC*c=[[ComponentCalloc]init];self.components=@[a,b,c];for(NSObject*aComponentinself.components){[aComponentsetup];}}@endlikethis这样做有什么好处?遵循开放封闭原则,接口通常比实现更稳定,因此高层模块可以对修改关闭,对扩展开放。我们很容易在不修改高层模块的情况下替换或扩展新的底层实现。高内聚低耦合的代码不再受控制流依赖的限制,有利于插件化和组件化。例如:苹果的智能家居系统定义它支持Homekit接口,但不依赖于任何具体的Homekit产品。任何符合Homekit接口的产品都可以自由接入智能家居系统。但是,DIP原则只提供了架构设计的原则,并没有提供具体的实施措施。谁来创建底层模块?如何创建它?如何注入和绑定高层模块?多于