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

282

Cách đây nửa năm, chúng tôi đã bắt đầu chấp nhận Flux architecture trong ứng dụng PlanGrid iOS. Bài viết này sẽ phân tích động lực khiến chúng tôi chuyển đổi từ MVC truyền thống sang Flux và chia sẻ thêm những kinh nghiệm có được qua dự án trên.

Bằng cách phân tích code in production, tôi đang cố gắng miêu tả những phần quan trọng của Flux implementation. Nếu bạn quan tâm đến vấn đề ở cấp độ cao hơn thì có thể bỏ qua phần giữa của bài viết này.

Lý do chuyển đổi của chúng tôi

Để quyết định của chúng tôi phù hợp với ngữ cảnh, tôi muốn mô tả lại vài thách thức mà ứng dụng PlanGrid phải đối mặt. Một số thách thức chỉ dành cho phần mềm doanh nghiệp trong khi số khác có thể áp dụng vào hầu hết các ứng dụng iOS.

Chúng ta có tất cả State

PlanGrid là 1 ứng dụng iOS khá phức tạp. Nó cho phép người dùng xem các bản kế hoạch chi tiết và phối hợp trên các bản kế hoạch đó bằng cách sử dụng nhiều loại chú thích, vấn đề và attachments khác nhau (và rất nhiều thứ khác đòi hỏi kiến thức chuyên ngành).

Một khía cạnh quan trọng đầu tiên của ứng dụng là tính offline. Người dùng có thể tương tác với tất cả các tính năng trong ứng dụng dù có mạng Internet hay không. Vì vậy, chúng ta cần phải lưu trữ rất nhiều dữ liệu và trạng thái của khách hàng. Chúng ta cũng cần phải thực hiện 1 bộ các quy tắc kinh doanh nội bộ (ví dụ: chú thích nào mà 1 user có thể xóa?)

Ứng dụng PlanGrid chạy trên cả iPad và iPhone nhưng UI của nó được tối ưu hóa để tận dụng bộ nhớ có sẵn lớn hơn trên tablets. Khác với rất nhiều ứng dụng iPhone, chúng ta thường thể hiện nhiều view controllers cùng 1 lúc. Những view controller này có xu hướng chia sẻ 1 số lượng state.

Tình trạng quản lý State

Nói chung ứng dụng của bạn phải quản lý tốt các state. Bất kì thay đổi trong ứng dụng, không ít thì nhiều cũng sẽ dẫn đến những bước sau:

  1. Cập nhật trạng thái trong đối tượng nội bộ
  2. Cập nhật UI
  3. Cập nhật database.
  4. Sắp xếp loại thay đổi sẽ được gửi đến server theo kết nối mạng có sẵn
  5. Thông báo cho các đối tượng khác về sự thay đổi trạng thái

Tôi sẽ giải quyết tất cả các khía cạnh trong cấu trúc mới  trong những bài viết khác, riêng bài này tôi chỉ tập trung vào bước thứ 5. Làm thế nào để điền các cập nhật trạng thái trong ứng dụng của bạn?

Đây xứng đáng là câu hỏi tỷ đô trong lập trình ứng dụng.

Hầu hết các kỹ sư iOS, gồm các lập trình viên đời đầu của ứng dụng PlanGrid đều đề xuất những câu trả lời sau:

  • KVO
  • NSNotificationCenter
  • Callback Blocks
  • Sử dụng DB như dữ liệu chân lý (source of truth)

Tất cả phương pháp này sẽ hiệu quả trong những tình huống khác nhau. Tuy nhiên, các lựa chọn khác biệt này lại trở thành nguồn cơn gây bất ổn trong 1 codebase lớn đã phát triển qua nhiều năm.

Sự nguy hiểm của tự do

Classic MVC chỉ hỗ trợ tách biệt dữ liệu và phần hiển thị của dữ liệu đó. Nếu thiếu sự hướng dẫn cấu trúc khác, mỗi dev phải giải quyết tất cả những thứ còn lại.

Ứng dụng PlanGrid (như hầu hết các ứng dụng iOS) đã không có 1 pattern được xác định để quản lý state.

Rất nhiều công cụ quản lý state hiện hành như delegation và blocks có khuynh hướng tạo các dependencies mạnh mẽ giữa các thành tố không như mong muốn – 2 view controllers nhanh chóng được ghép cặp chặt chẽ để chia sẻ những cập nhật state với nhau.

Các công cụ khác, như KVO và Notifications, tạo các dependencies không thấy được. Khi bạn dùng chúng trong 1 codebase lớn, bạn có thể gặp những thay đổi về code và hiệu ứng phụ không như mong muốn. Một controller có thể quan sát các chi tiết của model layer mà chúng không nên hứng thú.

Các code reviews và style guides có thể hỗ trợ khá nhiều nhưng rất nhiều vấn đề về cấu trúc bắt nguồn từ những thứ lặt vặt thiếu nhất quán và chúng cũng mất 1 khoảng thời gian để trở thành những vấn đề nghiêm trọng. Với các patterns được xác định tốt, bạn có thể dễ dàng phát hiện sớm độ lệch chuẩn.

Một architectural pattern để quản trị State

Một trong những mục tiêu quan trọng nhất của chúng tôi trong suốt quá trình cấu trúc lại ứng dụng PlanGrid là làm rõ các patterns và các practices tốt nhất thích hợp. Điều này cho phép bạn viết các tính năng tương lai 1 cách nhất quán hơn và tích hợp những kĩ sư mới hiệu quả hơn.

Quản lý state là 1 trong những nguyên nhân lớn nhất gây nên sự phức tạp trong ứng dụng của chúng ta, vì thế chúng tôi đã quyết định sẽ xác định 1 pattern mà tất cả tính năng có thể dùng trong tương lai.

Những vấn đề trong codebase hiện tại rất giống các vấn đề mà Facebook đã từng gặp khi họ trình bày Flux pattern lần đầu tiên:

  • Các cập nhật state phân tầng, không như mong muốn
  • Rất khó để hiểu các dependencies giữa các components
  • Làm rối luồng thông tin
  • Dữ liệu chân lý (source of truth) không rõ ràng

Dường như Flux sẽ phù hợp nhất khi giải quyết nhiều vấn đề mà chúng ta đang đối mặt.

Giới thiệu ngắn về Flux

Flux là 1 pattern cấu trúc gọn nhẹ mà Facebook sử dụng cho các ứng dụng web cliend-side. Mặc dù có 1 reference implementation, Facebook lại nhấn mạnh vào ý tưởng Flux pattern thích hợp với implementation cụ thể này hơn.

Biểu đồ này làm rõ Flux pattern bằng các components flux khác nhau:

Trong cấu trúc Flux, 1 store là dữ liệu chân lý duy nhất (single source of truth) cho 1 phần nào đó của ứng dụng. Bất cứ khi nào state trong store được cập nhật, nó sẽ gửi 1 thay đổi event đến tất cả các views đăng kí cho đã đăng kí với store. View nhận những thay đổi chỉ thông qua 1 interface này được gọi bởi store.

Các cập nhật state chỉ có thể xảy ra thông qua các actions.

Một action thể hiện 1 thay đổi state được dự định trước, nhưng nó không tự mình thực thi thay đổi state. Tất cả các components muốn thay đổi bất kì state nào gửi 1 action đến dispatcher toàn cầu. Các stores đăng kí với dispatcher và cho nó biết hành động nào mà chúng quan tâm. Bất cứ khi nào 1 action được gửi đi, tất cả các stores liên quan sẽ nhận được.

Để đáp trả lại các actions, 1 vài stores sẽ cập nhật state của chúng và thông báo các views về state mới.

Cấu trúc Flux thực thi 1 dòng dữ liệu 1 chiều như được hiển thị trên biểu đồ ở trên. Nó cũng thực thi phân tích các mối bận tâm:

  • Các views sẽ chỉ nhận dữ liệu từ các stores. Bất kì lúc nào 1 store cập nhật, handler method trong view sẽ được gọi đến
  • Các views chỉ có thể thay đổi state bằng cách gửi đi các actions. Vì actions là miêu tả duy nhất của intents, nên business logic được ẩn từ view
  • Một store chỉ cập nhật state của nó khi nó nhận 1 action

Những constraints hỗ trợ thiết kế, phát triển và debug các tính năng mới dễ dàng hơn.

Flux trong PlanGrid dành cho iOS

Với ứng dụng iOS PlanGrid chúng ta đã chệch hướng 1 chút so với Flux reference implementation. Chúng tôi đảm bảo rằng mỗi store sẽ có 1 propertystate quan sát được. Khác với Flux implementation gốc, chúng tôi không xuất ra 1 thay đổi event khi 1 store cập nhật. Thay vào đó, các views quan sát property state của store. Bất cứ khi nào các views nhận thấy 1 state change, chúng sẽ cập nhật phản hồi như sau:

Đây là sự lệch chuẩn rất nhỏ so với Flux reference implementation, nhưng giải quyết được nó sẽ hỗ trợ nhiều cho những phần tiếp theo.

Với sự am hiểu về nền tảng của Flux architecture, hãy phân tích sâu hơn vài chi tiết của implementaton và những câu hỏi chúng ta cần trả lời khi thực thi Flux trong ứng dụng PlanGrid.

Phạm vi của 1 Store là gì?

Phạm vi của mỗi store đơn là 1 câu hỏi rất thú vị thường xuất hiện khi bạn sử dụng Flux pattern.

Vì Facebook ra mắt Flux pattern, các biến thể (variations) khác nhau đã được cộng đồng phát triển. Một trong số đó là Redux, lặp lại trong Flux pattern bằng cách xác thực mỗi ứng dụng chỉ nên có 1 single store. Store này lưu trữ trạng thái của toàn bộ ứng dụng (có rất nhiều điểm khác biệt nhỏ, tinh vi khác nằm ngoài phạm vi bài post này).

Nhờ ý tưởng của 1 single store nữa đơn giản hóa cấu trúc của nhiều ứng dụng mà Redux trở nên nổi tiếng. Trong Flux truyền thống, nhờ có rất nhiều stores mà các ứng dụng có thể chạy những trường hợp mà các ứng dụng cần combine state được lưu trữ trong các stores riêng biệt để render 1 view nào đó. Cách tiếp cận đó có thể nhanh chóng giới thiệu lại các vấn đề mà Flux pattern đã cố giải quyết, như các dependencies phức tạp giữa các components khác nhau trong 1 ứng dụng.

Đối với ứng dụng PlanGrid, chúng tôi vẫn quyết định làm việc với Flux truyền thống, thay vì sử dụng Redux. Chúng tôi không chắc liệu cách tiếp cận với 1 single store lưu toàn bộ trạng thái của ứng dụng sẽ scale như thế nào với 1 ứng dụng lớn như thế. Hơn nữa, chúng tôi đã xác định có rất ít inter-store dependencies nên việc cân nhắc lựa chọn Redux không còn quan trọng nữa.

Chúng tôi vẫn chưa xác định 1 nguyên tắc cứng rắn đối với phạm vi của mỗi store đơn.

Cho đến hiện tại, tôi có thể nhận diện 2 patterns trong codebase của chúng tôi:

  • Feature/View Specific Stores: Mỗi view controller (hoặc mỗi nhóm view controllers quan hệ mật thiest với nhau) sẽ nhận được chính store của nó. Mẫu store này bắt chước mẫu view specific state.
  • Shared State Stores: Chúng ta có các store lưu trữ và quản lý trạng thái được chia sẻ giữa nhiều views. Chúng tôi cố gắng giữ số lượng stores này ở mức tối thiểu. Một ví dụ của store như thế là IssueStore. Store này chịu trách nhiệm quản lý trạng thái của tất cả các vấn đề thấy được trong bản kế hoạch chi tiết được chọn lựa gần đây. Nhiều view hiển thị và tương tác với các vấn đề lấy thông tin từ store này. Những loại stores nay sẽ hoạt động như 1 truy vấn database update trực tiếp.

Chúng tôi hiện đang thực thi shared state stores đầu tiên của mình và vẫn đang suy nghĩ chọn lựa cách tốt nhất để bắt chước rất nhiều dependencies của các views khác nhau trong những loại stores này.

Nguồn: IDE Academy via Blog.Benjamin (còn tiếp)