[Part 2] Làm việc với Model và Dictionary trong ObjC và Swift

Ở bài Part 1 lần trước mình đã giới thiệu về Model và Dictionary trong ObjC. Mình có nêu ra một số ý tưởng về Model như:

  • Model tự init data dựa trên JSON mà không phải tự viết hàm initWithDictionary cho mỗi model mới khai báo dựa trên thư viện objc-runtime.
  • Model cho phép add các target cũng như cài đặt callback để tự động trigger events mỗi khi một thuộc tính của Model thay đổi giá trị bằng cách viết thư viện key-value observing. Vấn đề ở đây là nó sẽ không crash và tối ưu hoá hơn khi sử dụng cái mặc định của Apple.
  • Model lấy ý tưởng tương tự như một ActiveRecord trong Ruby on Rails.

Hôm nay mình sẽ nói rõ hơn làm thế nào để làm được điều này bằng thư viện objc-runtime.

Model tự init data dựa trên JSON (Dictionary)

Để init data được cho một model tao có 2 cách tiếp cận:

  • Cách dễ nhất là lặp qua hết các keys của Dictionary và lấy value sau đó set cho model.
  • Biết được model đó có bao nhiêu thuộc tính cũng như kiểu dữ liệu của từng thuộc tính rồi tiến hành lấy dữ liệu tương ứng từ Dictionary để set cho từng thuộc tính.

Cách tiếp cận ban đầu có lợi thế là dễ làm nhưng nó dễ gặp nhiều vấn đề như: lặp qua các key dư thừa không có trong model (giả sử dictionary có 5 key nhưng model chỉ có 3 thuộc tính). Không khớp kiểu dữ liệu giữa dictinonary và model (giả sử price trong dictionary là kiểu int trong khi của model là kiểu float). Cụ thể ta có ví dụ như sau:

Dictionary

 {
     name: "Huy",
     age: 23,
     city: "Saigon",
     country: "Vietnam",
     bio: "Milk Carrot"
 }

Model

{
    name: String,
    age: float,
    bio: String  
}

Ta thấy giả sử nếu viết một hàm init data mà lặp qua kết các keys của Dictionary để set giá trị cho model thì sẽ dư thừa key city, country cho mỗi lần init data. Hơn nữa kiểu dữ liệu về age sẽ không khớp (không thể xác định được age trong dictionary là kiểu int hay kiểu float). Việc lặp qua hết các keys của Dictionary để set value cho thuộc tính model còn gặp một vấn đề nữa là nếu không handle exception thì chương trình sẽ bị crash khi key đó không có trong model ví dụ key citycountry

Với cách tiếp cận thứ hai là căn cứ vào thuộc tính của model để set giá trị có vẻ như là tốt hơn. Nhưng vấn đề là làm thế nào để lấy được danh sách thuộc tính cũng như kiểu dữ liệu của từng thuộc tính. Sử dụng thư viện objc-runtime. Các bước như sau:

Lấy danh sách thuộc tính -> lấy danh sách kiểu dữ liệu -> Lấy giá trị ứng với thuộc tính trong Dictionary -> Căn cứ vào kiểu dữ liệu ứng với thuộc tính tiến hành init dữ liệu cho thuộc tính.

  • Lấy danh sách thuộc tính:
# hàm lấy danh sách thuộc tính của một class trong objc-runtime

objc_property_t *properties = class_copyPropertyList([self class], &outCount);

# lặp qua lần lượt các thuộc tính để lấy ra kiểu dữ liệu
for(int i = 0; i < outCount; i++) {  
    objc_property_t property = properties[i];
    objc_property_t property = properties[i];
    const char *propName = property_getName(property);
....
}
  • Sau khi lấy ra được thuộc tính và kiểu dữ liệu ta có thể tiến hành lấy giá trị và set giá trị cho model.
# lấy giá trị ứng với thuộc tính trong Dictionary
id value = [dictionary valueForKey:propertyName];

# gán giá trị cho thuộc tính đó trong model.
[self setValue:value forKey:propertyName];

Toàn bộ source code bạn có thể xem ở đây: model

Tạo property reaction khi property thay đổi giá trị

Đôi khi trong lập trình, để dễ dàng hơn, ta muốn khi giá trị một thuộc tính của model thay đổi thì nó sẽ tiến hành gọi một hàm nào đó (callback). Để làm được điều này, chúng ta sử dụng tính năng key-value observing của ObjC cung cấp. Giả sử ta gọi hàm callback đó là một Action.

  • Tạo Object class cho Action:
@interface SModelAction : NSObject

// Đặc tả tên thuộc tính của model mà có sự thay đổi
@property (nonatomic, copy) NSString *keyPath;

// Đặc tả sự thay đổi (add/remove/init/changed...)
@property (nonatomic) SModelEvent event;

// Con trỏ của objective gọi hàm callback
@property (nonatomic, weak) id target;

// Hàm callback sẽ được target gọi
@property (nonatomic) SEL selector;

@end
  • Tiến hành đăng kí một event cho một thuộc tính của model
- (void)property:(NSString *)property
          target:(id)target
        selector:(SEL)selector
         onEvent:(SModelEvent)event {

    // Kiểm tra xem action đã được đăng kí trước hay chưa, nếu đã đăng kí rồi thì báo trùng lập và không làm gì cả
    if ([[self getActionsOfProperty:property
                             target:target
                           selector:selector
                            onEvent:event] count] > 0) {
#ifdef DEBUG
        NSLog(@"Duplicated register keyPath: %@", property);
#endif
        return;
    }

    // Đăng kí observer cho thuộc tính nếu chưa đăng ký
    [self registerObserverForKeyPath:property];

    // Tạo một đối tượng mô tả event và lưu lại
    SModelAction *modelAction = [[SModelAction alloc] init];
    modelAction.keyPath = property;
    modelAction.target = target;
    modelAction.selector = selector;
    modelAction.event = event;
    [[self actions] addObject:modelAction];
}
  • Hàm đăng ký key-observer
- (void)registerObserverForKeyPath:(NSString *)keyPath {
    if (![self keyPathExisted:keyPath]) {
        @synchronized(self) {
            [self addKeyPath:keyPath];
            [self addObserver:self
                   forKeyPath:keyPath
                      options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                      context:SPreadContext];
            }
    }
}
  • Cài đặt hàm thực thi khi nhận được notification
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    id oldValue = change[@"old"];
    id newValue = change[@"new"];
    SModelEvent event = SModelEventOnChange;

    NSMutableArray *actionsToDelete = [NSMutableArray 
    NSArray *actions = [self getActionsOfProperty:keyPath
                                              array];
onEvent:event];  
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        for (SModelAction *action in actions) {
            id target = action.target;
            if (target) {
                ((void (*)(id, SEL))[target methodForSelector:action.selector])(target,
                                                                                action.selector);
            } else {
                [actionsToDelete addObject:action];
            }
        }
        for (SModelAction *action in actionsToDelete) {
            [self removeActionsForProperty:action.keyPath
                                    target:action.target];
        }
    }];
}
  • Khi giá trị của model bị thay đổi giả sử trong đoạn code sau:
User *user = [[User init] alloc];  
[user property:"name" target:self selector:@selector(renderNameLabel) onEvent:SModelEventOnChange];

// Gán giá trị mới cho field name
user.name = "New name";  

Sau dòng lệnh gán giá trị mới cho thuộc tính name trong model user, thì model user sẽ tiến hành gọi hàm renderNameLabel. Model cung cấp một cơ chế tự động remove key-observer nên khi sử dụng chỉ cần add target, không cần quan tâm tới việc remove key-observer khi dealloc model. Điều này giảm thiểu việc crash khi lập trình.

Toàn bộ source code bạn có thể xem ở đây

Model lấy ý tưởng tương tự như một ActiveRecord trong Ruby on Rails

Trong model ở đây, mình chỉ lấy ý tưởng từ phần active record về việc auto mapping giữa tên, kiểu dữ liệu của thuộc tính với dữ liệu trong JSON (Dictionary). Trong khi làm việc với các app có lấy dữ liệu từ mạng về, các bạn rất hay gặp trường hợp cần tải dữ liệu của một đối tượng về mà chỉ biết id của đối tượng đó. Ví dụ khởi tạo đối tượng User có id bằng 1.

User *user = User.findById(1)  

Hoặc bất đồng bộ

User *user = User()  
user.id = 1  
user.fetchInBackgroud()  

Các công việc cần làm là ta sử dụng một private networking cho class model để tiến hành lấy dữ liệu từ server và kết hợp với phần ở trên để init dữ liệu. Với ý tưởng này, ta có thể đóng gói được phần code làm việc với server gói gọn chỉ trong phần model base mà không cần phải viết lại cho mỗi lần tạo thêm model mới. Từ đó lượng code sinh ra ít hơn và dễ quản lý hơn.

Trong phần model (mình đã update thành opensource) mình đã hiện thực tất cả các phần ở trên, việc sử dụng cũng cực kì đơn giản, chỉ việc kết thừa từ class SModel là có tất cả các tiện ích kể trên. Đồng thời SModel cũng cung cấp thêm nhiều hàm tiện ích khác như mặc định gái trị default cho Model khi giá trị tron Dictionary bị null, convert ngược lại từ class model thành Dictionary cho việc gửi parameters hoặc lưu thành file JSON. Cung cấp hàm callback bằng block, thêm hàm callback khi fetchInBackground.

Huy

I gave away my old skin to hold you, as a new me. Love you, Cà Rốt Sữa

Saigon, [email protected]