07/09/2018, 15:55

Dependency Injection in Objective C - Typhoon Framework

Giới thiệu Typhoon Framework là 1 thư viện dependency injection (DI) cho Cocoa và CocoaTouch. Với ưu điểm là nhẹ và dễ sử dụng. Git: https://github.com/typhoon-framework/Typhoon 1. Dependency Injection là gì? Là 1 design pattern thực thi inversion of control (IoC). Một "injection" là việc ...

Giới thiệu

Typhoon Framework là 1 thư viện dependency injection (DI) cho Cocoa và CocoaTouch. Với ưu điểm là nhẹ và dễ sử dụng.

Git: https://github.com/typhoon-framework/Typhoon

1. Dependency Injection là gì?

Là 1 design pattern thực thi inversion of control (IoC). Một "injection" là việc đưa một đối tượng phụ thuộc (service) vào client. Đưa service vào client thay vì để client tìm và tạo service là yêu cầu cơ bản nhất của pattern này.

Ưu điểm

  • Do các client giảm sự phụ thuộc vào service nên dễ dàng hơn trong việc viết unit test.
  • Tăng khả năng tái sử dụng code, test và bảo trì.
  • Giúp chương trình có khả năng cấu hình hoạt động chương trình theo các file cấu hình mà không cần phải biên dịch lại.
  • Giảm các code khởi tạo

Nhược điểm:

  • Code khó đọc hiểu hơn, lập trình viên phải đọc cấu hình để hiểu được cách hoạt động của hệ thống.
  • Code dài hơn cách code truyền thống

Ví dụ:

Nếu không sử dụng DI:

- (id)init
{
    self = [super init];
    if (self)
    {
        _weatherClient = [[GoogleWeatherClientImpl alloc] initWithParameters:xyz];
    }
    return self;
}

Nếu dùng DI:

- (id)initWithWeatherClient:(id)weatherClient
{
    self = [super init];
    if (self)
    {
        _weatherClient = weatherClient;
    }
    return self;
}

Chương trình sẽ không còn phụ thuộc vào GoogleWeatherClient, khi cần sử dụng client khác ví dụ như Yahoo ta có thể dễ dàng cấu hình mà không phải thay đổi code trong chương trình.

2. Cài đặt Typhoon Framework

Đơn giản nhất là cài qua CocoaPods.

3. Ứng dụng

Ta sẽ tìm hiểu cách cài đặt, cấu hình và sử dụng Typhoon bằng cách xây dựng 1 chương trình quản lý người dùng TPUserManager.
Chương trình có các chức năng:

  • Liệt kê danh sách người dùng
  • Thêm người dùng
  • Xem chi tiết người dùng

3.1. Cài đặt

Tạo project ở XCode, theo template Single View Applycation (iOS) đặt tên là TPUserManager, tích chọn Use Core Data

Thêm Typhoon Framework vào project thông qua CocoaPods.

Mở TPUSerManager workspace.

3.2. Tạo core data model

Tạo model User có các thông số

@interface User : NSManagedObject

@property (nonatomic, retain) NSString * address;
@property (nonatomic, retain) NSDate * creation_time;
@property (nonatomic, retain) NSString * email;
@property (nonatomic, retain) NSString * full_name;
@property (nonatomic, retain) NSString * user_name;
@property (nonatomic, retain) NSString * user_id;

@end

3.3. Tạo đối tượng Dto

Thay vì làm việc trực tiếp với User model, ta sẽ sử dụng UserDto để giao tiếp trong chương trình.

@interface UserDto : NSObject

@property (nonatomic, retain) NSString * full_name;
@property (nonatomic, retain) NSDate * creation_time;
@property (nonatomic, retain) NSString * address;
@property (nonatomic, retain) NSString * email;
@property (nonatomic, retain) NSString * user_name;
@property (nonatomic, retain) NSString * user_id;

@end

3.4. Viết Mapper

Mapper chuyển dữ liệu qua lại giữa User và UserDto

#import <Foundation/Foundation.h>
@class User;
@class UserDto;

@interface Mapper : NSObject

+ (UserDto *)userDtoFromUser:(User *)user;
+ (void)mapFromUserDto:(UserDto *)userDto toUser:(User *)user;

@end

3.5. Viết user service

3.5.1. User Protocol

Ta sẽ khai báo các chức năng cần có của user service thông qua protocol:

#import <Foundation/Foundation.h>
@class CreateUserInput;
@class CreateUserOutput;
@class GetUsersInput;
@class GetUsersOutput;
@class GetUserInput;
@class GetUserOutput;

@protocol UserServiceProtocol

- (CreateUserOutput *)createUser: (CreateUserInput *)input;

- (GetUsersOutput *)getUsers: (GetUsersInput *)input;

- (GetUserOutput *)getUser: (GetUserInput *)input;

@end

Các chức năng gồm có:

  • Liệt kê danh sách user: getUsers, hàm cần tham số GetUsersInput và trả về GetUsersOutput. Ta sẽ viết tham số và kết quả trả về thành 1 class Input, Output riêng để nếu cần thay đổi tham số sẽ không phải sửa lại khai báo hàm.
// GetUsersInput.h

#import <Foundation/Foundation.h>

@interface GetUsersInput : NSObject

@end

// GetUsersOutput.h

#import <Foundation/Foundation.h>

@interface GetUsersOutput : NSObject

@property (nonatomic, copy) NSArray *users;

@end
  • Lấy thông tin 1 user:
// GetUserInput.h

#import <Foundation/Foundation.h>

@interface GetUserInput : NSObject

@property (nonatomic) NSString *userId;

@end
// GetUserOutput.h

#import <Foundation/Foundation.h>
@class UserDto;

typedef NS_ENUM(NSInteger, GetUserResult) {
    GetUserResultSuccess, GetUserResultUserNotFound
};

@interface GetUserOutput : NSObject

@property (nonatomic) GetUserResult result;
@property (nonatomic) UserDto *user;

@end

3.5.2. Implement UserServiceProtocol

Ta sẽ viết 2 class, 1 class để test và 1 class dùng Core Data

// FakeUserService.h

#import <Foundation/Foundation.h>
#import "UserServiceProtocol.h"

@interface FakeUserService : NSObject

@end
// FakeUserService.m

#import "FakeUserService.h"
#import "CreateUserInput.h"
#import "CreateUserOutput.h"
#import "GetUsersInput.h"
#import "GetUsersOutput.h"
#import "UserDto.h"
#import "GetUserInput.h"
#import "GetUserOutput.h"

@interface FakeUserService() {
    NSMutableDictionary *_users;
}

@end

@implementation FakeUserService

- (instancetype)init {
    self =[super init];
    if (self) {
        _users = [NSMutableDictionary new];

        UserDto *user1 = [[UserDto alloc] init];
        user1.user_id = [[NSUUID UUID] UUIDString];
        user1.user_name = @"tuan";
        user1.full_name = @"Truong Anh Tuan";
        [_users setObject:user1 forKey:user1.user_id];

        UserDto *user2 = [[UserDto alloc] init];
        user2.user_id = [[NSUUID UUID] UUIDString];
        user2.user_name = @"fun";
        user2.full_name = @"Fun Kun";

        [_users setObject:user2 forKey:user2.user_id];
    }
    return self;
}

- (CreateUserOutput *)createUser: (CreateUserInput *)input {
    CreateUserOutput *result = [CreateUserOutput new];

    UserDto *user = [[UserDto alloc] init];
    user.user_id = [[NSUUID UUID] UUIDString];
    user.user_name = input.user_name;
    user.full_name = input.full_name;
    user.email = input.email;
    user.address = input.address;

    [_users setObject:user forKey:user.user_id];

    NSLog(@"users1 = %@", _users);

    result.result = CreateUserResultSuccess;

    return result;
}

- (GetUsersOutput *)getUsers: (GetUsersInput *)input {
    GetUsersOutput *result = [GetUsersOutput new];

    result.users = [_users allValues];

    NSLog(@"users2 = %@", _users);

    return result;
}

- (GetUserOutput *)getUser: (GetUserInput *)input {
    GetUserOutput *result = [GetUserOutput new];
    result.user = [_users objectForKey:input.userId];
    result.result = result.user ? GetUserResultSuccess : GetUserResultUserNotFound;

    return result;
}

@end
// DataCoreUserService.h

#import <Foundation/Foundation.h>
#import "UserRepositoryProtocol.h"

@interface DataCoreUserService : NSObject

@property (nonatomic) id  userRepository;

@end
// DataCoreUserService.m

#import "DataCoreUserService.h"
#import "CreateUserInput.h"
#import "CreateUserOutput.h"
#import "GetUsersInput.h"
#import "GetUsersOutput.h"
#import "UserDto.h"
#import "GetUserInput.h"
#import "GetUserOutput.h"
#import "User.h"
#import "Mapper.h"
#import <CoreData+MagicalRecord.h>

@implementation DataCoreUserService

- (CreateUserOutput *)createUser: (CreateUserInput *)input {
    CreateUserOutput *result = [CreateUserOutput new];

    User *user = [User MR_createEntity];
    user.user_id = [[NSUUID UUID] UUIDString];
    user.user_name = input.user_name;
    user.full_name = input.full_name;
    user.email = input.email;
    user.address = input.address;

    NSManagedObjectContext *context = [NSManagedObjectContext MR_defaultContext];
    [context MR_saveOnlySelfAndWait];

    result.result = CreateUserResultSuccess;

    return result;

}

- (GetUsersOutput *)getUsers: (GetUsersInput *)input {
    GetUsersOutput *result = [GetUsersOutput new];
    NSArray* users = [self.userRepository getAllUsers];
    NSMutableArray *userDtos = [NSMutableArray new];
    for (User *user in users) {
        UserDto *userDto = [Mapper userDtoFromUser:user];
        [userDtos addObject:userDto];
    }
    result.users = userDtos;

    return result;
}

- (GetUserOutput *)getUser: (GetUserInput *)input {
    return nil;
}

@end

DataCoreUserService sử dụng DataCoreUserRepository để giao tiếp với CoreData

// UserRepositoryProtocol.h

#import <Foundation/Foundation.h>
@class User;

@protocol UserRepositoryProtocol

- (void)insertUser:(User *)user;
- (void)updateUser:(User *)user;
- (void)deleteUser:(User *)user;
- (void)deleteUserWithId:(int)userId;

- (NSArray *)getAllUsers;

@end
// DataCoreUserRepository.h

#import <Foundation/Foundation.h>
#import "UserRepositoryProtocol.h"

@interface DataCoreUserRepository : NSObject

@end

// DataCoreUserRepository.m

#import "DataCoreUserRepository.h"
#import "User.h"
#import <CoreData+MagicalRecord.h>

@implementation DataCoreUserRepository

- (void)insertUser:(User *)user {

}
- (void)updateUser:(User *)user {

}
- (void)deleteUser:(User *)user {

}
- (void)deleteUserWithId:(int)userId {

}

- (NSArray *)getAllUsers {
    return [User MR_findAll];
}

@end

3.6. Tạo giao diện

3.6.1. RootViewController

ss1.jpg

Kế thừa UITableViewController. RootViewController dùng userService để lấy danh sách User cũng như các thao tác thêm, sửa, xóa.

// RootViewController.h

#import <UIKit/UIKit.h>
#import "UserServiceProtocol.h"

@interface RootViewController : UITableViewController

@property (nonatomic) id  userService;

- (IBAction)onAddButtonClicked:(id)sender;

@end
// RootViewController.m

#import "RootViewController.h"
#import "GetUsersOutput.h"
#import "GetUsersInput.h"
#import "UserDto.h"
#import "UserDetailsViewController.h"
#import "CreateUserViewController.h"

@interface RootViewController ()
{
    NSArray *users;
}

- (IBAction)onRefreshButtonClicked:(id)sender;

@end

@implementation RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"user service %@", self.userService);

    users = [[_userService getUsers:nil] users];

    NSLog(@"users = %@", users);
    NSLog(@"user service 1 = %@", self.userService);

}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)refreshUI {
    users = [[_userService getUsers:nil] users];
    [self.tableView reloadData];

    NSLog(@"user service 2 = %@", self.userService);
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [users count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UserCell" forIndexPath:indexPath];

    if (cell) {
        UserDto *user = [users objectAtIndex:indexPath.row];
        cell.textLabel.text = user.full_name;
        cell.detailTextLabel.text = user.user_name;
    }

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    UserDto *user = [users objectAtIndex:indexPath.row];
    if (user) {
        [self performSegueWithIdentifier:@"ShowUserDetailsViewController" sender:user];
    }

}

#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

    if ([segue.identifier isEqualToString:@"ShowUserDetailsViewController"]) {
        UserDetailsViewController *controller = segue.destinationViewController;
        controller.user = sender;
    }
    else if ([segue.identifier isEqualToString:@"ShowCreateUserViewController"]) {
        CreateUserViewController *controller = segue.destinationViewController;
        controller.delegate = self;
        controller.userService = self.userService;
    }

}

- (IBAction)onAddButtonClicked:(id)sender {
    [self performSegueWithIdentifier:@"ShowCreateUserViewController" sender:self];
}

#pragma mark - CreateUserViewControllerDelegate

- (void)didCreateUser:(UserDto *)user {
    [self refreshUI];
}

- (void)didUpdateUser:(UserDto *)user {
    [self refreshUI];
}

- (IBAction)onRefreshButtonClicked:(id)sender {
    [self refreshUI];
}
@end

3.6.2. CreateUserViewController

// CreateUserViewController.h

#import <UIKit/UIKit.h>
#import "UserServiceProtocol.h"
@class UserDto;

@protocol CreateUserViewControllerDelegate

- (void)didCreateUser: (UserDto *)user;
- (void)didUpdateUser: (UserDto *)user;

@end
// CreateUserViewController.m

@interface CreateUserViewController : UITableViewController

@property (nonatomic, weak) id  delegate;

@property (nonatomic) UserDto *user;
@property (nonatomic) id  userService;

@end

#import "CreateUserViewController.h"
#import "CreateUserInput.h"
#import "CreateUserOutput.h"
#import "UserDto.h"

@interface CreateUserViewController ()

@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;

@property (weak, nonatomic) IBOutlet UITextField *fullnameTextField;

@property (weak, nonatomic) IBOutlet UITextField *emailTextField;

@property (weak, nonatomic) IBOutlet UITextField *addressTextField;

@property (nonatomic) BOOL isCreateUser;

- (IBAction)onDoneButtonClicked:(id)sender;

- (IBAction)onCancelButtonClicked:(id)sender;

@end

@implementation CreateUserViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self refreshUI];

    if (!self.user) {
        self.isCreateUser = YES;
    }
    else {
        self.isCreateUser = NO;
    }

}

- (void)refreshUI {
    if (self.user) {
        self.usernameTextField.text = self.user.user_name;
        self.fullnameTextField.text = self.user.full_name;
        self.addressTextField.text = self.user.address;
        self.emailTextField.text = self.user.email;
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)onDoneButtonClicked:(id)sender {

    if (self.isCreateUser) {
        [self createUser];
    }
    else {
        [self updateUser];
    }
}

- (IBAction)onCancelButtonClicked:(id)sender {
    [self close];
}

- (void)updateUser {
    [self.delegate didUpdateUser:self.user];
}

- (void)createUser {
    CreateUserInput *input = [CreateUserInput new];
    input.user_name = self.usernameTextField.text;
    input.full_name = self.fullnameTextField.text;
    input.email = self.emailTextField.text;
    input.address = self.addressTextField.text;

    CreateUserOutput *output = [self.userService createUser:input];
    switch (output.result) {
        case CreateUserResultSuccess:
            [self.delegate didCreateUser:self.user];
            [self close];
            break;
        case CreateUserResultInvalidParameters:
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"ERROR!" message:@"Invalid parameters" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];

            break;
        default:
            break;
    }

}

- (void)close {
    [self.navigationController popViewControllerAnimated:YES];
}

@end

3.6.3. UserDetailsViewController

// UserDetailsViewController.h

#import

@class UserDto;

@interface UserDetailsViewController : UITableViewController

@property (nonatomic) UserDto *user;

@end
// UserDetailsViewController.m

#import "UserDetailsViewController.h"
#import "UserDto.h"

@interface UserDetailsViewController ()

@property (weak, nonatomic) IBOutlet UILabel *usernameLabel;

@property (weak, nonatomic) IBOutlet UILabel *fullnameLabel;

@property (weak, nonatomic) IBOutlet UILabel *emailLabel;

@property (weak, nonatomic) IBOutlet UILabel *addressLabel;

@end

@implementation UserDetailsViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self refreshUI];
}

- (void)refreshUI {
    if (self.user) {
        self.usernameLabel.text = self.user.user_name;
        self.fullnameLabel.text = self.user.full_name;
        self.addressLabel.text = self.user.address;
        self.emailLabel.text = self.user.email;
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

3.7. Cấu hình Typhoon Framework - "trộn" tất cả lại

3.7.1. Tạo class kế thừa TyphoonAssembly

Đây là class "boiler plate", thông qua class này mỗi khi chương trình cần instance của 1 class nào, Typhoon sẽ cung cấp instance tương ứng kèm các cấu hình (nếu có).

// TPApplicationAssembly.h

#import "TyphoonAssembly.h"
#import "UserRepositoryProtocol.h"
#import "UserServiceProtocol.h"

@class RootViewController;
@class CreateUserViewController;

@interface TPApplicationAssembly : TyphoonAssembly

- (id)userRepository;

- (id)userService;

- (RootViewController *)rootViewController;

- (CreateUserViewController *)createUserViewController;

@end

Cấu hình để chương trình sử dụng CoreData (real):

// TPApplicationAssembly.m

#import "TPApplicationAssembly.h"
#import "FakeUserService.h"
#import "RootViewController.h"
#import "CreateUserViewController.h"
#import "DataCoreUserRepository.h"
#import "DataCoreUserService.h"

@implementation TPApplicationAssembly

- (id)userRepository {
    return [TyphoonDefinition withClass:[DataCoreUserRepository class] configuration:^(TyphoonDefinition *definition) {

    }];
}

- (id)userService {
    return [TyphoonDefinition withClass:[DataCoreUserService class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(userRepository) with:[self userRepository]];
    }];
}

- (RootViewController *)rootViewController {
    return [TyphoonDefinition withClass:[RootViewController class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(userService) with:[self userService]];
    }];
}

- (CreateUserViewController *)createUserViewController {
    return [TyphoonDefinition withClass:[CreateUserViewController class] configuration:^(TyphoonDefinition *definition) {

    }];
}

@end

Cấu hình "fake", dùng để test:

// TPApplicationAssembly.m

#import "TPApplicationAssembly.h"
#import "FakeUserService.h"
#import "RootViewController.h"
#import "CreateUserViewController.h"
#import "DataCoreUserRepository.h"
#import "DataCoreUserService.h"

@implementation TPApplicationAssembly

- (id)userRepository {
    return nil;
}

- (id)userService {
    return [TyphoonDefinition withClass:[FakeUserService class] configuration:^(TyphoonDefinition *definition) {

    }];
}

- (RootViewController *)rootViewController {
    return [TyphoonDefinition withClass:[RootViewController class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(userService) with:[self userService]];
    }];
}

- (CreateUserViewController *)createUserViewController {
    return [TyphoonDefinition withClass:[CreateUserViewController class] configuration:^(TyphoonDefinition *definition) {

    }];
}

@end

3.7.2. Cấu hình trong Info.plist để Typhoon dùng class TPApplicationAssembly này khi chương trình chạy

Thêm key TyphoonInitialAssemblies, type Array, có value Item 0 = TPApplicationAssembly

3.8. Chạy chương trình

Chạy và thay đổi cấu hình TPApplicationAssembly theo Real và Fake, chúng ta thấy chương trình hoạt động giống nhau trong 2 trường hợp. Tuy nhiên khi thoát app thì dữ liệu chỉ được lưu trong trường hợp Real.

ss2.jpg

Chúng ta cũng có thể mở rộng chương trình để thay vì dùng CoreData có thể dùng web service.

4. Kết luận

Với DI pattern và Typhoon Framework, chúng ta có thể viết được 1 chương trình cho iOS theo mô hình đa lớp, giảm được tối thiểu sự phụ thuộc giữa các lớp và cho phép cấu hình thay đổi hoạt động của chương trình.

Tất nhiên là tìm 1 đáp án chung cho tất cả các bài toán là rất khó, chương trình cũng có nhiều hạn chế ví dụ như không dùng được sức mạnh của NSFetchedResultsController trong việc hiển thị danh sách user (vì cái này phụ thuộc vào Core Data).

Rất mong được sự góp ý của các bạn. Cảm ơn các bạn đã theo dõi.

0