Thế giới Flux Architecture trong iOS (phần cuối)

77

PHẦN 1PHẦN 2

(tiếp Step 4: Gắn kết UI với Store ở phần 2)

tableViewModelForState là 1 pure function, có input là state mới nhất và trả lại 1 đoạn mô tả của table view dưới dạng FluxTableViewModel. Cách thức hoạt động của method này tương tự như render function của React. FluxTableViewModel độc lập với UIKit và là 1 simple struct miêu tả nội dung của table. Bạn có thể tìm thấy implementation ví dụ nguồn mở trong AutoTable repository.

Sau đó, kết quả của method này sẽ gắn với property tableViewDataSource của view controller. Component lưu trữ trong property đó chịu trách nhiệm cập nhật UITableView dựa trên thông tin đã được cung cấp trong FluxTableViewModel.

Binding code khác thì dễ hơn, như code kích hoạt/ vô hiệu button “Clear Filer” dựa trên state isFiltering:

Thực hiện (implement) các UI bindings chính là 1 trong những phần khó nhất, vì nó không hoàn toàn phù hợp với mô hình programming của UIKit. Nhưng bạn cần chỉ viết vài components tùy chỉnh là có thể dễ dàng thực hiện UI bindings. Thay vì gắn liền với phương pháp MVC truyền thống thực thi các cập nhật UI qua rất rất nhiều view controllers thì chúng tôi đã từng tiết kiệm rất nhiều thời gian nhờ thực thi những components này.

Với các UI bindings phù hợp, chúng tôi đã phân tích phần cuối của implementing 1 tính năng Flux. Tôi muốn viết lại recap lại trước khi chuyển sang phân tích phương pháp testing cho các tính năng Flux.

Implementation Recap

Khi thực thi 1 tính năng Flux, tôi thường chia công việc thành nhiều phân mảng như sau:

  1. Nhận diện hình dáng của state type
  2. Nhận diện các actions
  3. Thực thi business logic và các diễn hoạt state cho mỗi actions – implementation này tồn tại trong store
  4. Thực thi UI bindings phác thảo biểu đồ state đến view presentation

Tiếp theo, chúng tôi sẽ phân tích cách test các tính năng Flux

Writing Tests

Một trong những ưu điểm chính của Flux architecture là nó tách riêng các vấn đề, nên cũng dễ test business logic và code UI lớn hơn.

Mỗi tính năng Flux có 2 vùng chính cần test:

  1. Business logic trong store
  2. View model providers (đây là những functions như React, sản xuất ra 1 miêu tả của UI dựa trên 1 input state)

Testing Stores

Testing stores thường rất đơn giản. Chúng ta có thể thu hút những tương tác với store bằng cách truyền các actions vào, theo dõi các thay đổi state bằng cách đăng kí store hoặc quan sát property _state nội bộ trong tests của mình.

Ngoài ra, còn có thể bắt chước types bên ngoài bất kì mà store cần phải đối thoại cùng để thực thi 1 tính năng nào đó (có thể là API client hoặc 1 đối tượng tiếp cận data) và gắn những types đó vào trong initializer của store. Nó cho phép chúng ta xác thực rằng những types này được gọi đến khi cần.

Trong ứng dụng PlanGrid, chúng tôi dùng Quick and Nimble để viết các test. Dưới đây là ví dụ đơn giản về test trong annotation filter store specification :

Một lần nữa, tuy tôi không đào sâu chi tiết về test cụ thể này nhưng tinh thần testing cần phải rõ ràng. Chúng tôi gửi các actions đến store và xác thực trả lời dưới dạng những thay đổi state hoặc các calls đến các mocks được chèn vào.

(Có lẽ bạn sẽ tự hỏi tại sao chúng tôi lại đang gọi method _handleActions trong store thay vì gửi 1 action sử dụng dispatcher. Thông thường, dispatcher sử dụng dispatch bất đồng bộ khi gửi các actions, đồng nghĩa là tests của chúng tôi cũng cần bất đồng bộ. Vì thế, chúng tôi đã trực tiếp gọi handler trong store. Việc thực thi (implementation) của dispatcher đã thay đổi, vì vậy chúng tôi có thể sử dụng dispatcher trong các tests).

Bây giờ, khi thực thi business logic trong 1 store, tôi hầu như sẽ viết tests của mình trước. Structure của store code của chúng tôi, cùng với behavioral Quick specs rất phù hợp với quy trình lập trình dạng test.

Testing Views

Flux architecture, kết hợp với declarative UI layer hỗ trợ testing views dễ dàng hơn. Chúng tôi vẫn đang tranh luận mục tiêu nhắm đến trong view layer.

Trên thực tế, tất cả view code của chúng tôi khá dễ hiểu. Nó kết nối state trong store với các properties khác nhau của UI layer. Đối với ứng dụng của mình, chúng tôi đã quyết định giải quyết hầu hết code này qua các UI automation tests.

Tuy nhiên, vẫn có rất nhiều lựa chọn. Vì view layer được thiết lập để render 1 state đã chèn vào, nên snapshot test cũng hoạt động rất tốt. Artsy đã từng đề cập đến ý tưởng snapshot testing trong rất nhiều bài nói và blog posts, kể cả bài viết rất hay trên objc.io.

Khi nhận thấy đã sử dụng đủ UI automation, chúng tôi không cần thêm snapshot tests nữa.

Tôi cũng đã thử nghiệm unit testing các hàm view provide (như: hàm tableViewModelForState chúng ta đã thấy ở trước). Nhưng view providers này là những pure functions vạch sơ đồ từ 1 state đến mô tả UI, nên rất dễ test dựa trên 1 input và 1 giá trị trả về. Tuy nhiên, tôi nhận ra nhưng tests này không có nhiều ý nghĩa vì chúng phản ánh mô tả khai báo của implementation 1 cách chặt chẽ.

Sử dụng Flux architecture view testing khá đơn giản vì view code được tách biệt hoàn toàn với phần còn lại của ứng dụng. Bạn chỉ cần chèn 1 state cần được render trong tests và bạn đã sẵn sàng.

Vì có rất nhiều lựa chọn để testing UI nên tôi rất háo hức muốn biết các bạn (và nhưng lập trình viên khác) se chọn công cụ nào nếu xét về lâu dài.

Kết luận

Sau khi chia thành nhiều implementation details, tôi muốn kết lại bằng kinh nghiệm mà chúng tôi có được.

Chúng tôi chỉ mới sử dụng Flux architecture được khoảng 6 tháng, nhưng đã nhận ra khá nhiều lợi điểm có được code base của mình:

  • Các tính năng mới được thực thi 1 cách đồng bộ. Cấu trúc của các stores, view providers và view controllers qua các tính năng gần như giống hệt nhau
  • Khi quan sát state, actions và các tests dạng BDD, bạn có thể hiểu ngay cách hoạt động của 1 tính năng
  • Những vấn đề giữa stores và views sẽ được tách biệt hoàn toàn. Hầu như bạn sẽ không phải mập mờ nên đặt code nào đó ở đâu
  • Code của chúng tôi đọc dễ hơn. State mà view phụ thuộc sẽ luôn minh bạch, rõ ràng. Nhờ đó, quá trình debug cũng dễ dàng hơn.
  • Tất cả những điểm trên sẽ giúp các lập trình viên mới dễ tiếp thu hơn

Tất nhiên, vẫn có những khuyết điểm

  • Đầu tiên và trước nhất là việc tích hợp với UIKit components có thể khó khăn hơn 1 chút. Khác với các React components, UIKit views không cung cấp 1 API chỉ để tự cập nhật dựa trên state mới. Gánh nặng này buộc chúng tôi phải thực thi nó thủ công trong view bingdings hoặc phải viết các components tùy chỉnh wrap UIKit components
  • Không phải tất cả code mới của chúng tôi đều làm theo Flux pattern. Ví dụ: chúng tôi vẫn chưa giải quyết được hệ thống điều hướng (navigation)/ định tuyến (routing) hoạt động với Flux. Chúng tôi cần phải tích hợp 1 coordinator pattern vào Flux architeture hoặc sử dụng 1 router thực sự tương tự ReSwift Router.
  • Chúng tôi cần phải nghĩ ra 1 pattern tốt cho state để chia sẻ qua nhiều phần lớn của ứng dụng. Chúng tôi có nên có dependencies giữa các stores như trong Flux pattern gốc? Những lựa chọn thay thế là gì?

Tôi rất muốn nghiên cứu sâu về các implementation details, những điểm thuận lợi và bất lợi. Vì vậy, tôi hy vọng có thể nghiên cứu vài khía cạnh 1 cách chi tiết hơn trong những bài post khác.

Hy vọng rằng bài viết này sẽ giúp bạn hiểu thêm là liệu Flux architecture có phù hợp với mình hay không.

Tham khảo: 

  • Flux – Trang web Flux chính thức trên Facebook gồm đoạn giới thiệu gốc về Flux
  • Unidirectional Data Flow in Swift – 1 bài viết của tôi về Swift liên quan đến Redux concepts và ReSwift implementation gốc
  • ReSwift – 1 implementation của Redux trong Swift
  • ReSwift Router – 1 router khai báo cho các ứng dụng ReSwift

Nguồn: IDE Academy via Blog.Benjamin