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

强心剂——依赖注入

时间:2023-03-16 17:01:28 科技观察

什么是依赖注入(DependencyInjection)?依赖注入(DependencyInjection,DI)是Java、C#等很多编程语言中流行的设计模式,但在Objective-C中并没有被广泛使用。本文旨在通过使用Objective-C实例来简单介绍依赖注入,同时介绍在Objective-C代码中使用依赖注入的实用方法。虽然本文主要针对Objective-C,但所有提到的概念也适用于Swift。依赖注入的概念很简单:对象应??该通过依赖获得,而不是自己创建。建议将MartinFowler关于该主题的出色讨论作为背景阅读。可以通过初始化器(或构造器)或属性(设置方法)将依赖项传递给对象。它们通常被称为“构造函数注入”和“setter注入”。(构造函数注入和set方法注入)ConstructorInjection:-(instancetype)initWithDependency1:(Dependency1*)d1dependency2:(Dependency2*)d2;SetterInjection:@property(nonatomic,retain)Dependency1*dependency1;@property(nonatomic,retain)依赖2*依赖2;根据Fowler的描述,一般情况下首选构造函数注入,不适合构造函数注入时选择setter注入。在使用构造函数注入时,您很可能仍会为这些依赖项定义属性,您可以通过将这些属性设置为只读来简化对象API。为什么要使用依赖注入?使用依赖注入有很多好处:1.依赖声明清晰。一个对象需要执行什么操作一目了然,并且很容易消除危险的隐藏依赖,例如全局变量。2.组件化。依赖注入提倡组合而不是继承,以提高代码的可重用性。3.更容易定制。创建对象时,在特殊情况下更容易部分自定义对象。4.明确隶属关系。特别是在使用构造函数依赖注入时,严格执行对象所有权规则——可以构建直接的非循环对象图。5.易于测试。依赖注入比任何其他方法都更能提高对象的可测试性。由于通过构造函数创建这些对象很简单,因此无需管理隐藏的依赖项。此外,模拟依赖关系变得很容易,这样测试就可以集中在被测试的对象上。在您的代码中使用依赖注入您的代码库可能尚未使用依赖注入设计模式,但很容易切换。依赖注入的一大优点是您不需要对整个项目代码都使用这种模式。相反,您可以在代码库的特定区域使用它并从那里扩展。第二层各种类型的注入首先类分为两种类型:基本类型和复杂类型。原始类型没有依赖关系,或者只依赖于其他原始类型。基本类型基本不继承,因为它们的功能明确不变,不需要链接外部资源。很多基本类型都派生自Cocoa本身,例如NSString、NSArray、NSDictionary和NSNumber。复杂类型则相反。它们具有复杂的依赖关系,包括应用程序级逻辑(需要修改),或对额外资源的访问,例如磁盘、网络或全局内存服务。应用程序中的大多数类都很复杂,包括几乎所有的控制器对象和模型对象。许多Cocoa类型也很复杂,例如NSURLConnection或UIViewController。根据上面的分类,使用依赖注入模式最简单的方法是先在应用程序中选择一个复杂的类,找到该类中其他复杂对象被初始化的地方(找到“alloc]init”或“new"关键字)。在类中引入依赖注入,改变类中传递的实例化对象作为初始化参数,而不是类初始化对象本身。在初始化时分配依赖项让我们看一个示例,其中子对象(依赖项)在父级的初始化函数中被初始化。原代码如下:@interfaceRCRaceCar()@property(nonatomic,readonly)RCEngine*engine;@end@implementationRCRaceCar-(instancetype)init{...//Createtheengine.Notethatitcannotbecustomizedor//mockedoutwithoutmodifyingtheinternalsofRCRaceCar._engine=[[RCEnginealloc]init];returnself;}@end依赖注入做了一个小修改:@interfaceRCRaceCar()@property(nonatomic,readonly)RCEngine*engine;@end@implementationRCRaceCar//Theengineiscreatedbeforetheracecarandpassedin//asaparameter,andthecallercancustomizeitifdesired.-(instancetype)initWithEngine:(RCEngine*)engine{..._engine=engine;returnsself;}@end懒初始化依赖于一些对象,可能过段时间再用,或者初始化后会用到,或者永远不会用到。依赖注入前的示例:@interfaceRCRaceCar()@property(nonatomic)RCEngine*engine;@end@implementationRCRaceCar-(instancetype)initWithEngine:(RCEngine*)engine{..._engine=engine;returnsself;}-(void)recoverFromCrash{if(self.fire!=nil){RCFireExtinguisher*fireExtinguisher=[[RCFireExtinguisheralloc]init];[fireExtinguisherextinguisherFire:self.fire];}}@end一般车子不会撞车,所以我们永远不会用我们的灭火器。由于需要这个对象的概率很低,我们不想通过在初始化方法中立即创建它们来减慢每辆车的创建速度。此外,如果我们的汽车需要从多次碰撞中恢复过来,这将需要创建多个灭火器。对于这种情况,我们可以使用工厂设计模式。工厂设计模式是标准的ObjectIce-C块语法,它不带参数并返回一个对象的实例。一个对象可以使用它们的块创建依赖关系,而无需知道它们是如何创建的细节。下面是一个使用依赖注入的例子,即使用工厂设计模式来创建我们的灭火器:非原子,复制,只读)RCFireExtinguisherFactoryfireExtinguisherFactory;@end@implementationRCRaceCar-(实例类型)initWithEngine:(RCEngine*)enginefireExtinguisherFactory:(RCFireExtinguisherFactory)extFactory{..._engine=引擎;_fireExtinguisherFactory=[extFactorycopy];returnselid.fire!=nil){RCFireExtinguisher*fireExtinguisher=self.fireExtinguisherFactory();[fireExtinguisherextinguishFire:self.fire];}}@end当我们需要创建未知数量的依赖项时,工厂模式也很有用,即使是在初始化控制器中创建的,例如:@implementationRCRaceCar-(instancetype)initWithEngine:(RCEngine*)enginetransmission:(RCTransmission*)transmissionwheelFactory:(RCWheel*(^)())wheelFactory;{self=[superinit];if(self==nil){returnnil;}_engine=engine;_transmission=transmission;_leftFrontWheel=wheelFactory();_leftRearWheel=wheelFactory();_rightFrontWheel=wheelFactory();_rightRearWheel=wheelFactory();//保留wheelfactory以备后用incaseweneedaspare._wheelFactory=[wheelFactorycopy];returnsself;}@end#p#避免繁琐的配置如果该对象不应该在其他对象中alloc,那么它应该在Edge哪里正在分配?这样的依赖很难配置吗?每次分配它们都同样困难吗?这些问题的解决依赖于类型的简洁初始化器(如+[NSDictionarydictionary]),我们将我们的对象图配置从普通对象中抽离出来,使它们纯净且可测试,并且在添加之前业务逻辑清晰类型的简单初始化方法,确保它是必要的。如果一个对象在init方法中只有少量的参数,而且这些参数没有合理的默认值,那么这个类型不需要引入初始化方法,直接调用标准的init方法即可。我们将从四个地方的依赖项配置我们的对象:没有合理默认值的值。例如,每个实例可能包含不同的布尔值或数值。这些值应该作为参数传递给类型的简洁初始化器。现有的共享对象。这些对象应该作为参数传递给类型的简洁初始化器(例如无线电波)。这些对象之前可能已被评估为单例或通过超类指针。新创建的对象。如果我们的对象不能与其他对象共享这些依赖关系,那么协作对象应该在类型配置文件初始化函数中创建一个新实例。这些是以前在对象的实现中直接分配的对象。系统单例。这些是cocoa提供的单例和可以直接使用的单例。这些单例的应用,比如[NSFileManagerdefaultManager],在你的app中,预计只需要使用单例生成类型的单个实例。系统中有很多这样的单例。一个赛车类的简洁初始化方法如下:(^wheelFactory)()=^{return[[RCWheelalloc]init];};return[[selfalloc]initWithEngine:enginetransmission:transmissionpitRadioFrequency:frequencywheelFactory:wheelFactory];}你的类型便利初始化器应该就位。常用或可重用的配置文件将作为对象放在.m文件中,而特定Foo对象使用的配置器应放在RaceCar的@interface中。系统单例在Cocoa库中的众多对象中只有一个实例,如[UIApplicationsharedApplication]、[NSFileManagerdefaultManager]、[NSUserDefaultsstandardUserDefaults]、[UIDevicecurrentDevice]。如果一个对象依赖于上述对象,则应将其放入设备的初始化参数中。即使您的代码中可能只有一个实例,您的测试也希望模拟该实例或创建测试实例以避免测试相互依赖。建议您避免在自己的代码中创建全局引用的单例,并且不要在第一次需要对象时创建对象的单个实例或注入依赖于它的所有对象。不可变构造函数偶尔会出现的问题是类的初始化器/构造函数无法更改或直接调用。在这种情况下,应该使用setter注入,例如://我们仍然可以使用属性来配置ourracetrack.raceTrack.width=10;这在对象设计中引入了额外的可变性,需要对其进行测试和解决。幸运的是,可以避免导致无法访问或不可变初始化的两种主要情况。类注册使用类注册工厂模式,这意味着对象不能修改它们的初始值设定项。NSArray*raceCarClasses=@[[RCFastRaceCarclass],[RCSlowRaceCarclass],];NSMutableArray*raceCars=[[NSMutableArrayalloc]init];for(ClassraceCarClassinraceCarClasses){//Allracecarsmustthavethesameinitializer("init"inthiscase).//这意味着我们不能自定义不同的子类。:[[raceCarClassalloc]init]];}对于此类问题,只需将类型声明列表替换为工厂模式块即可。typedefRCRaceCar*(^RCRaceCarFactory)();NSArray*raceCarFactories=@[^{return[[RCFastRaceCaralloc]initWithTopSpeed:200];},^{return[[RCSlowRaceCaralloc]initWithLeatherPlushiness:11];}];NSMutableArray*raceCars=[[NSMutableArrayalloc]init];for(RCRaceCarFactoryraceCarFactoryinraceCarFactories){//Wenownolongercarewhichinitializerisbeingcalled.[raceCarsaddObject:raceCarFactory()];}Storyboardsstoryboards提供了一种方便的方式来布局我们的用户界面,但它们给依赖注入带来了问题。特别是,在Storyboard中初始化ViewController不允许您选择调用哪个初始化方法。同样,在sytoyboard中定义页面跳转时,targetViewController也没有给你自定义的初始化方法来生成实例。解决方案是避免使用故事板。这听起来像是一个极端的解决方案,但我们会发现使用故事板会产生许多其他问题。另外,如果不想失去storyboard给我们带来的便利,可以使用XIB,XIB允许自定义initializer。公共与。私有依赖注入鼓励您在公共接口中公开更多对象。如前所述,这有很多优点。在构建框架时,它可以极大地丰富您的公共API。并且使用依赖注入,公共对象A可以使用私有对象B(反过来可以使用私有对象C),但对象B和C永远不会暴露在框架之外。对象A依赖于在初始化器中注入对象B,然后对象B的构造函数创建一个公共对象C.//InpublicObjectA.h.@interfaceObjectA//因为initializersarereferencetoObjectBweneedto//maketheObjectBheaderpublic我们以前不会有的。-(instancetype)initWithObjectB:(ObjectB*)objectB;@end@interfaceObjectB//同样这里:weneedtoexposeObjectC.h.-(instancetype)initWithObjectC:(ObjectC*)objectC;@end@interfaceObjectC-(instancetype)init;@end你也不想要用户框架担心ObjectB和ObjectC的实现细节,我们可以通过协议来解决这个问题。@interfaceObjectA-(instancetype)initWithObjectB:(id)objectB;@end//这个协议只公开原始ObjectB的一部分//ObjectA需要的部分。我们没有创建对//我们的具体ObjectB(或ObjectC)实现的硬依赖.@protocolObjectB-(void)methodNeededByObjectA-非常适合依赖注入c和后来的Swift。正确使用可以使您的代码库更具可读性、易于测试和易于维护。