MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的依赖注入与模块化设计

2022-01-076.4k 阅读

一、理解依赖注入

1.1 依赖注入的概念

依赖注入(Dependency Injection,简称 DI)是一种设计模式,它的核心思想是将对象所依赖的其他对象通过外部传递进来,而不是在对象内部自行创建。这种方式使得对象之间的依赖关系更加清晰,提高了代码的可测试性、可维护性和可扩展性。

在传统的编程方式中,对象往往在内部创建它所依赖的对象,例如:

@interface UserService : NSObject
- (void)fetchUser;
@end

@implementation UserService
- (void)fetchUser {
    NetworkManager *manager = [[NetworkManager alloc] init];
    // 使用 manager 进行网络请求获取用户数据
}
@end

在上述代码中,UserService 类依赖于 NetworkManager 类来进行网络请求获取用户数据。但是,UserService 类直接在内部创建了 NetworkManager 的实例,这就导致了 UserService 类与 NetworkManager 类之间存在紧密耦合。如果需要更换网络请求的方式,例如从使用 NetworkManager 切换到另一个网络库,就需要修改 UserService 类的代码。

而依赖注入的方式则是将 NetworkManager 的实例通过外部传递给 UserService

@interface UserService : NSObject
- (instancetype)initWithNetworkManager:(NetworkManager *)manager;
- (void)fetchUser;
@property (nonatomic, strong) NetworkManager *networkManager;
@end

@implementation UserService
- (instancetype)initWithNetworkManager:(NetworkManager *)manager {
    self = [super init];
    if (self) {
        _networkManager = manager;
    }
    return self;
}
- (void)fetchUser {
    // 使用 self.networkManager 进行网络请求获取用户数据
}
@end

通过这种方式,UserService 类不再关心 NetworkManager 是如何创建的,只关心它能提供的功能。这样,在需要更换网络请求方式时,只需要创建一个新的符合 NetworkManager 接口的类,并将其作为参数传递给 UserService 即可,而无需修改 UserService 类的代码。

1.2 依赖注入的优点

  1. 提高可测试性:在测试 UserService 时,如果 UserService 内部自行创建 NetworkManager,那么在测试时很难模拟 NetworkManager 的行为。而通过依赖注入,可以很方便地传入一个模拟的 NetworkManager 对象,从而对 UserService 进行单元测试。
@interface MockNetworkManager : NetworkManager
// 重写相关网络请求方法以返回模拟数据
@end

@implementation MockNetworkManager
// 模拟网络请求返回数据
@end

// 测试 UserService
- (void)testFetchUser {
    MockNetworkManager *mockManager = [[MockNetworkManager alloc] init];
    UserService *service = [[UserService alloc] initWithNetworkManager:mockManager];
    [service fetchUser];
    // 断言验证 service 的行为
}
  1. 增强可维护性:由于对象之间的依赖关系更加清晰,当需要修改某个依赖对象的实现时,只需要在创建该依赖对象并进行注入的地方进行修改,而不会影响到依赖它的其他对象的代码。
  2. 提升可扩展性:当系统需要引入新的功能或者替换某个功能模块时,通过依赖注入可以很容易地将新的依赖对象注入到相关的类中,而不需要对大量的代码进行修改。

二、Objective-C 中实现依赖注入的方式

2.1 构造函数注入

构造函数注入是最常见的依赖注入方式之一,就像前面 UserService 的例子一样,通过类的构造函数(init 方法)将依赖对象传递进来。

@interface DatabaseService : NSObject
- (instancetype)initWithDatabase:(Database *)database;
- (void)saveData:(id)data;
@property (nonatomic, strong) Database *database;
@end

@implementation DatabaseService
- (instancetype)initWithDatabase:(Database *)database {
    self = [super init];
    if (self) {
        _database = database;
    }
    return self;
}
- (void)saveData:(id)data {
    [self.database save:data];
}
@end

在使用 DatabaseService 时:

Database *realDatabase = [[Database alloc] init];
DatabaseService *service = [[DatabaseService alloc] initWithDatabase:realDatabase];
[service saveData:@"Some important data"];

构造函数注入的优点是依赖关系在对象创建时就已经明确,使得代码的可读性和可维护性较好。但是,如果一个类有多个依赖对象,构造函数可能会变得非常复杂,参数列表很长。

2.2 属性注入

属性注入是通过对象的属性来注入依赖对象。

@interface LoggerService : NSObject
@property (nonatomic, strong) Logger *logger;
- (void)logMessage:(NSString *)message;
@end

@implementation LoggerService
- (void)logMessage:(NSString *)message {
    [self.logger log:message];
}
@end

在使用时:

LoggerService *loggerService = [[LoggerService alloc] init];
FileLogger *fileLogger = [[FileLogger alloc] init];
loggerService.logger = fileLogger;
[loggerService logMessage:@"This is a log message"];

属性注入的优点是灵活性较高,对象创建后可以随时更换依赖对象。缺点是依赖关系不那么直观,在对象创建时依赖对象可能还未注入,可能会导致运行时错误。

2.3 方法注入

方法注入是通过对象的方法来传入依赖对象。

@interface NotificationService : NSObject
- (void)sendNotification:(NSString *)message withSender:(NotificationSender *)sender;
@end

@implementation NotificationService
- (void)sendNotification:(NSString *)message withSender:(NotificationSender *)sender {
    [sender send:message];
}
@end

使用时:

NotificationService *notificationService = [[NotificationService alloc] init];
EmailSender *emailSender = [[EmailSender alloc] init];
[notificationService sendNotification:@"New message" withSender:emailSender];

方法注入通常用于依赖对象只在特定方法调用时才需要的情况,它的优点是更加灵活,缺点是每次调用方法都需要传入依赖对象,可能会导致代码冗余。

三、模块化设计概述

3.1 模块化设计的概念

模块化设计是将一个大型的软件系统分解成多个独立的、可管理的模块。每个模块都有自己明确的职责,并且通过定义良好的接口与其他模块进行交互。

在 Objective-C 中,模块可以是一个类、一组相关的类,或者是一个框架。例如,一个社交应用可能有用户模块、消息模块、好友模块等。用户模块负责处理与用户相关的操作,如注册、登录等;消息模块负责处理消息的发送、接收等;好友模块负责管理好友关系。

模块化设计的核心原则包括:

  1. 单一职责原则:每个模块应该只负责一项特定的功能,这样可以降低模块的复杂度,提高模块的可维护性。例如,UserService 类只负责处理用户相关的业务逻辑,不应该同时处理网络请求和数据库操作等其他职责。
  2. 高内聚:模块内部的元素(类、方法等)应该紧密相关,共同完成模块的核心功能。例如,在 UserService 类中,所有的方法都应该围绕用户相关的操作,如 fetchUserupdateUser 等。
  3. 低耦合:模块之间的依赖关系应该尽量简单和松散。通过依赖注入等方式,可以降低模块之间的耦合度,使得一个模块的修改不会对其他模块造成太大的影响。

3.2 模块化设计的优点

  1. 提高开发效率:开发团队可以并行开发不同的模块,因为每个模块的开发相对独立。例如,一组开发人员可以专注于用户模块的开发,另一组开发人员可以同时开发消息模块。
  2. 便于维护和扩展:当需要修改某个功能时,可以直接定位到对应的模块进行修改,而不会影响到其他无关的模块。同时,在系统需要添加新功能时,可以很容易地创建新的模块并与现有模块进行集成。
  3. 提高代码复用性:良好的模块化设计使得模块可以在不同的项目中复用。例如,一个通用的网络请求模块可以被多个不同的应用项目所使用。

四、依赖注入与模块化设计的结合

4.1 在模块化设计中应用依赖注入

在模块化设计中,依赖注入是实现低耦合的关键技术。以一个电商应用为例,假设存在一个 CartModule 模块负责处理购物车相关的业务逻辑,它依赖于 ProductService 模块来获取商品信息,依赖于 UserService 模块来获取用户信息。

// ProductService 模块接口
@interface ProductService : NSObject
- (Product *)fetchProductWithID:(NSUInteger)productID;
@end

// UserService 模块接口
@interface UserService : NSObject
- (User *)fetchCurrentUser;
@end

// CartModule 模块
@interface CartModule : NSObject
- (instancetype)initWithProductService:(ProductService *)productService userService:(UserService *)userService;
- (void)addProductToCartWithID:(NSUInteger)productID;
@property (nonatomic, strong) ProductService *productService;
@property (nonatomic, strong) UserService *userService;
@end

@implementation CartModule
- (instancetype)initWithProductService:(ProductService *)productService userService:(UserService *)userService {
    self = [super init];
    if (self) {
        _productService = productService;
        _userService = userService;
    }
    return self;
}
- (void)addProductToCartWithID:(NSUInteger)productID {
    Product *product = [self.productService fetchProductWithID:productID];
    User *user = [self.userService fetchCurrentUser];
    // 执行添加商品到购物车的逻辑
}
@end

通过依赖注入,CartModule 模块不关心 ProductServiceUserService 是如何实现的,只关心它们能提供的接口。这样,当 ProductServiceUserService 的实现发生变化时,只要接口不变,CartModule 模块不需要进行修改。

4.2 依赖注入对模块化设计的提升

  1. 增强模块的独立性:依赖注入使得每个模块都可以独立地进行开发、测试和维护。例如,CartModule 模块可以在不依赖于具体的 ProductServiceUserService 实现的情况下进行单元测试,只需要传入模拟的 ProductServiceUserService 对象即可。
  2. 促进模块的复用:由于模块之间通过依赖注入实现了低耦合,一个模块可以更容易地被复用在不同的项目或系统中。例如,ProductService 模块可以被复用在电商应用的不同版本,或者被其他类似的购物应用所使用。
  3. 便于系统的扩展和升级:当系统需要添加新的功能模块或者替换现有模块时,依赖注入使得集成过程更加简单。例如,如果要替换 ProductService 模块为一个新的版本,只需要在创建 CartModule 时传入新的 ProductService 实例即可,而不需要对 CartModule 的内部代码进行大规模修改。

五、实际项目中的应用案例

5.1 一个移动应用项目

假设我们正在开发一个移动办公应用,该应用包含多个模块,如用户登录模块、文件管理模块、日程管理模块等。

  1. 用户登录模块:依赖于网络请求模块来验证用户登录信息。
// 网络请求模块接口
@interface NetworkRequest : NSObject
- (void)sendLoginRequestWithUsername:(NSString *)username password:(NSString *)password completion:(void(^)(BOOL success))completion;
@end

// 用户登录模块
@interface LoginModule : NSObject
- (instancetype)initWithNetworkRequest:(NetworkRequest *)request;
- (void)loginWithUsername:(NSString *)username password:(NSString *)password;
@property (nonatomic, strong) NetworkRequest *networkRequest;
@end

@implementation LoginModule
- (instancetype)initWithNetworkRequest:(NetworkRequest *)request {
    self = [super init];
    if (self) {
        _networkRequest = request;
    }
    return self;
}
- (void)loginWithUsername:(NSString *)username password:(NSString *)password {
    [self.networkRequest sendLoginRequestWithUsername:username password:password completion:^(BOOL success) {
        if (success) {
            // 登录成功处理
        } else {
            // 登录失败处理
        }
    }];
}
@end
  1. 文件管理模块:依赖于本地存储模块来保存和读取文件,依赖于网络请求模块来上传和下载文件。
// 本地存储模块接口
@interface LocalStorage : NSObject
- (void)saveFile:(NSData *)fileData withName:(NSString *)fileName;
- (NSData *)fetchFileWithName:(NSString *)fileName;
@end

// 文件管理模块
@interface FileModule : NSObject
- (instancetype)initWithLocalStorage:(LocalStorage *)localStorage networkRequest:(NetworkRequest *)networkRequest;
- (void)saveFileLocally:(NSData *)fileData withName:(NSString *)fileName;
- (NSData *)fetchFileLocallyWithName:(NSString *)fileName;
- (void)uploadFile:(NSData *)fileData withName:(NSString *)fileName;
- (void)downloadFileWithName:(NSString *)fileName;
@property (nonatomic, strong) LocalStorage *localStorage;
@property (nonatomic, strong) NetworkRequest *networkRequest;
@end

@implementation FileModule
- (instancetype)initWithLocalStorage:(LocalStorage *)localStorage networkRequest:(NetworkRequest *)networkRequest {
    self = [super init];
    if (self) {
        _localStorage = localStorage;
        _networkRequest = networkRequest;
    }
    return self;
}
- (void)saveFileLocally:(NSData *)fileData withName:(NSString *)fileName {
    [self.localStorage saveFile:fileData withName:fileName];
}
- (NSData *)fetchFileLocallyWithName:(NSString *)fileName {
    return [self.localStorage fetchFileWithName:fileName];
}
- (void)uploadFile:(NSData *)fileData withName:(NSString *)fileName {
    // 使用 networkRequest 进行文件上传
}
- (void)downloadFileWithName:(NSString *)fileName {
    // 使用 networkRequest 进行文件下载
}
@end

通过依赖注入,各个模块之间的依赖关系清晰,每个模块都可以独立开发和测试。例如,在测试 LoginModule 时,可以传入一个模拟的 NetworkRequest 对象来验证登录逻辑是否正确,而不需要实际进行网络请求。

5.2 项目中的依赖管理

在实际项目中,随着模块数量的增加,依赖管理变得尤为重要。可以使用一些工具来辅助管理依赖,如 CocoaPods。CocoaPods 可以帮助我们管理项目中的第三方库依赖,同时也可以用于管理项目内部模块之间的依赖。

例如,假设 LoginModule 依赖于一个第三方的加密库 CryptoLib,可以在 Podfile 中进行如下配置:

target 'MyOfficeApp' do
    pod 'CryptoLib'
end

对于项目内部模块之间的依赖,也可以通过类似的方式进行管理。例如,假设 FileModule 依赖于 LoginModule(可能是为了在上传文件前验证用户登录状态),可以在 FileModulePodfile 中进行配置:

target 'FileModule' do
    pod 'LoginModule', :path => '../LoginModule'
end

这样,通过 CocoaPods 可以方便地管理项目中模块之间的依赖关系,确保各个模块使用的依赖版本一致,同时也便于项目的部署和维护。

六、注意事项与常见问题

6.1 依赖循环问题

在使用依赖注入和模块化设计时,可能会出现依赖循环的问题。例如,ModuleA 依赖于 ModuleB,而 ModuleB 又依赖于 ModuleA。这种情况下,会导致对象创建时出现死循环。

@interface ModuleA : NSObject
- (instancetype)initWithModuleB:(ModuleB *)moduleB;
@property (nonatomic, strong) ModuleB *moduleB;
@end

@interface ModuleB : NSObject
- (instancetype)initWithModuleA:(ModuleA *)moduleA;
@property (nonatomic, strong) ModuleA *moduleA;
@end

// 尝试创建对象时会出现死循环
ModuleA *moduleA = [[ModuleA alloc] initWithModuleB:[[ModuleB alloc] initWithModuleA:moduleA]];

解决依赖循环问题的方法有:

  1. 重构模块:分析模块之间的依赖关系,看是否可以通过调整模块的职责来消除循环依赖。例如,将 ModuleAModuleB 中相互依赖的部分提取到一个新的模块 ModuleC 中,使得 ModuleAModuleB 都依赖于 ModuleC,而不是相互依赖。
  2. 使用弱引用:在某些情况下,可以使用弱引用来打破循环依赖。例如,将 ModuleB 中对 ModuleA 的引用改为弱引用。
@interface ModuleB : NSObject
- (instancetype)initWithModuleA:(ModuleA *)moduleA;
@property (nonatomic, weak) ModuleA *moduleA;
@end

但是,使用弱引用需要注意对象生命周期的管理,确保在使用弱引用对象时它仍然存在。

6.2 依赖对象的创建和管理

在依赖注入中,需要考虑依赖对象的创建和管理问题。如果在多个地方都创建相同的依赖对象,可能会导致资源浪费和不一致性。例如,在多个模块中都创建 NetworkManager 对象来进行网络请求。 解决这个问题的方法可以是使用单例模式来创建依赖对象,确保整个应用中只有一个实例。

@interface NetworkManager : NSObject
+ (instancetype)sharedManager;
- (void)sendRequest:(NSURLRequest *)request completion:(void(^)(NSData *data, NSError *error))completion;
@end

@implementation NetworkManager
+ (instancetype)sharedManager {
    static NetworkManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManager = [[self alloc] init];
    });
    return sharedManager;
}
- (void)sendRequest:(NSURLRequest *)request completion:(void(^)(NSData *data, NSError *error))completion {
    // 网络请求逻辑
}
@end

在需要使用 NetworkManager 的模块中:

@interface SomeModule : NSObject
- (instancetype)init;
- (void)doSomething;
@end

@implementation SomeModule
- (instancetype)init {
    self = [super init];
    if (self) {
        // 使用单例的 NetworkManager
        NetworkManager *manager = [NetworkManager sharedManager];
    }
    return self;
}
- (void)doSomething {
    NetworkManager *manager = [NetworkManager sharedManager];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com"]];
    [manager sendRequest:request completion:^(NSData *data, NSError *error) {
        // 处理请求结果
    }];
}
@end

这样可以保证整个应用中只有一个 NetworkManager 实例,避免了资源浪费和不一致性问题。

6.3 过度依赖注入

虽然依赖注入有很多优点,但过度使用依赖注入也可能带来问题。例如,将所有的依赖都通过构造函数注入,可能会导致构造函数参数列表过长,代码可读性变差。

@interface ComplexModule : NSObject
- (instancetype)initWithDependency1:(Dependency1 *)dependency1 dependency2:(Dependency2 *)dependency2 dependency3:(Dependency3 *)dependency3 /*... 更多依赖 */;
@end

在这种情况下,应该考虑是否可以将一些依赖进行合理分组,或者是否某些依赖可以通过其他方式(如属性注入)来提供,以提高代码的可读性和可维护性。

同时,过度依赖注入可能会导致代码变得过于复杂,增加理解和调试的难度。因此,在使用依赖注入时,需要根据项目的实际情况进行权衡,确保在获得依赖注入优点的同时,不会引入过多的复杂性。