iOS · redux

Giới thiệu Redux trong Swift

Trong vài năm trở lại đây, single page application cùng những ứng dụng giàu tính tương tác với người dùng đã và đang trở nên khá phổ biến. Chúng ta phải đối mặt với nhiều vấn đề nằm ngoài khả năng đáp ứng của những mô hình lập trình lâu đời như MVC, MVVM. Chính vì thế, một số mô hình mới đã ra đời và được đón nhận từ cộng đồng như: Flux, Viper, Redux, CQRS và Event Sourcing… Trong loạt bài này, mình sẽ giới thiệu về một mô hình còn khá mới, nhưng cũng đang nhận được sự chú ý khá lớn từ cộng đồng đó là Redux.

Phần 1: Giới thiệu Redux
Phần 2: Sử dụng Redux trong Swift: Action, Store, Reducer
Phần 3: Sử dụng Middleware, ActionCreator

1. Vấn đề với MVC

1.1. Vấn đề đồng bộ dữ liệu giữa các chức năng

Giả sử ban đầu ứng dụng có chức năng danh sách người dùng và chức năng thêm/ xóa/ sửa. Một nhu cầu thường gặp là mỗi khi thêm/xóa/sửa một người dùng thì chức năng danh sách người dùng cần phải được đồng bộ để hiển thị dữ liệu mới nhất. Sau đó, chúng ta thêm mới chức năng chat, đòi hỏi danh sách người dùng cần được đồng bộ tại chức năng chat mỗi khi có thao tác thêm/ xoá/ sửa. Khi ứng dụng được mở rộng, nhu cầu đồng bộ dữ liệu giữa các chức năng có thể chồng chéo như mô hình sau:

flux-react-mvc

Lưu ý: Sơ đồ không thể hiện mối quan hệ giữa các thành phần trong MVC, mà thể hiện mối quan hệ giữa việc thay đổi modelnhu cầu đồng bộ hóa hiển thị ở các view liên quan.

Hệ quả:

  • Vấn đề đồng bộ dữ liệu giữa các chức năng trở thành bài toán khó khi ứng dụng có nhiều chức năng cùng tham chiếu tới một nguồn dữ liệu.
  • Khó khăn trong việc mở rộng, thêm mới chức năng. Khi đó, chúng ta có thể phải cân nhắc tới việc đồng bộ hoá dữ liệu ở các chức năng mới, thậm chí là đồng bộ dữ liệu ở chức năng cũ khi có sự thay đổi ở chức năng mới.
  • Ngoài ra, việc đồng bộ dữ liệu dẫn tới việc phụ thuộc lẫn nhau giữa các chức năng (circular dependency) như hình vẽ trên.

Nguyên nhân dẫn tới việc này là trong MVC dữ liệu bị phân tán tại nhiều nơi, một điểm quan trọng hơn nữa đó là việc sử dụng two-way binding (bidirectional data flow) dẫn tới dữ liệu có thể bị thay đổi ở bất kỳ nơi nào trong luồng dữ liệu đó. Nó cản trở cho việc quan sát, kiểm soát sự thay đổi để thông báo cho các bên liên quan.

Mvvm

1.2. Quản lý trạng thái ứng dụng (State)

Vấn đề tiếp theo mà MVC gặp phải đó chính là việc thiếu một cơ chế quản lý trạng thái của ứng dụng. Ngoài việc sử dụng luồng dữ liệu hai chiều, thì đây cũng là nguyên nhân chính dẫn tới vấn đề khó khăn trong việc đồng bộ hoá dữ liệu như vừa nêu trên.

Trạng thái của ứng dụng bao gồm: trạng thái của view (loading, current active route, ..) và dữ liệu. Trong MVC, chúng ta thường quản lý trạng thái phân tán tại: database, view stack, singleton, …

Hệ quả:  chúng ta có thể gặp phải những khó khăn trong các trường hợp sau:

  • Cập nhật và đồng bộ hoá dữ liệu giữa các chức năng (như đã nêu ở phần 1.1)
  • Chia sẻ dữ liệu giữa các controller. Khi thêm chức năng mới, chức năng cũ cần phải thay đổi để truyền/chia sẻ dữ liệu với chức năng mới.
  • Thực hiện các bài toán Undo/Redo/Time Travelling.

1.3. Controller kiêm nhiệm nhiều nhiệm vụ

Sơ đồ bên dưới diễn tả mô hình MVC được áp dụng phổ biến.

mvc_role_diagram

Từ hình vẽ trên, ta có thể thấy rằng, controller đóng vai trò trung tâm trong mô hình MVC, chịu trách nhiệm kết nối tất cả các thành phần trong ứng dụng: nhận kết quả từ model, kiểm tra tính hợp lệ của dữ liệu, cập nhật view, model, … Khi phát triển ứng dụng cho iOS, chúng ta thấy rõ hơn vấn đề này trong Massive View Controller mà Apple phát triển.

Cho dù chúng ta có một kiến trúc tốt, thì ít nhất controller cũng cần phải được thực hiện như sau:

func login(username: String, password: String) {
  api.authenticate(username, password: password) { response, error in
    if (error == nil) {
      let nextViewController = ...
      navigationController.pushViewController(nextViewController)
    } 
    else {
      showErrorMessage(error)
    }
  }
}

Có thể thấy là controller phải đứng chờ kết quả trả về, nó cũng cần phải biết chi tiết về business, ý nghĩa của kết quả trả về là gì, các lỗi gì có thể xảy ra, … để từ đó có thể thực hiện những tác vụ cần thiết như hiển thị lỗi hoặc cập nhật view.

massiveviewcontroller

1.4. Kết luận

Ngày nay, MVC đã và đang được sử dụng rộng rãi, bản thân mình có thể vẫn tiếp tục sử dụng MVC để phát triển các ứng dụng Web. Nhưng khi phát triển những ứng dụng single page application hoặc ứng dụng cho điện thoại, có thể mình sẽ cân nhắc tới những mô hình mới để khắc phục những vấn đề mà mình đã nêu trên.

2. Giới thiệu Redux

Redux được Dan Abramov giới thiệu vào khoảng tháng 6 năm 2014, được viết bằng Javascript (2 Kb) cho các ứng dụng single page application (Redux is a predictable state container for JavaScript apps) dựa trên mô hình Flux, CQRS và Event Sourcing. Sau đó, Benjamin Encz giới thiệu một framework trong Swift là ReSwift.

Ý tưởng của Redux khá đơn giản, được mô tả như hình bên dưới. Mục tiêu của Redux là tập trung trạng thái và sự thay đổi tại một điểm, để từ đó có thể dễ dàng kiểm soát. Một điều quan trọng hơn nữa đó là Redux sử dụng dòng dữ liệu một chiều (Unidirectional Data Flow) như hình bên dưới:

 

Redux Flow

Redux ra đời dựa trên CQRS và Event Sourcing cùng với ba nguyên lý sau:

2.1. Single source of truth

Trạng thái của ứng dụng được lưu trữ trong một cây trạng thái duy nhất, được gọi là Store

struct AppState {
   var isLoading: Bool
   var count: Int
}

Việc lưu trữ dữ liệu tại một điểm duy nhất, giúp cho việc quan sát và kiểm soát trạng thái của ứng dụng thuận tiện hơn, các vấn đề về đồng bộ dữ liệu được giải quyết triệt để.

Ngoài ra, việc này giúp chúng ta biết chính xác trạng thái của ứng dụng tại một thời điểm. Các bài toán khó giải quyết trước đây như Undo, Redo, Time Travelling thì nay sẽ dễ dàng được giải quyết do chúng ta có thể biết được chính xác trạng thái trước đó của ứng dụng.

2.2. State is read-only

Cách duy nhất để thay đổi trạng thái của ứng dụng là thông qua một action, đó là một đối tượng diễn tả điều chúng ta muốn thực hiện

enum CounterAction: Action {
    case increase
}
store.dispatch(CounterAction.increase)

Bởi vì mọi sự thay đổi được tập trung tại một điểm và xảy ra tuần tự, nên chúng ta có thể dễ dàng theo dõi sự thay đổi đó. Hơn nữa, action là một đối tượng dữ liệu (plain object) nên chúng ta có thể log, lưu trữ, hoặc sau đó replay lại cho mục đích debug, kiểm thử. Ví dụ, khi ghi log chúng ta có thể theo dõi được tiến trình như sau:

state 0 -> action 1 -> state 1 -> action 2 -> state 2,...

Vì lý do này, khi chúng ta xem các ví dụ về Redux trong Swift, sẽ thấy các đối tượng dữ liệu, trạng thái ứng dụng thường sử dụng struct thay vì class. 

2.3. Changes are made with pure functions

Để thay đổi trạng thái của ứng dụng bởi mỗi một action, chúng ta sử dụng một pure function, gọi là reducer. (Xem thêm pure function trong bài Sử dụng ReactiveX trong Swift – Phần 2).

function handleAction(state: AppState, action: Action) {
    var newState = state
    switch (action) {
    case .increase:
        newState.count += 1
    }
    return newState
}

Việc sử dụng pure function, đảm bảo cho việc cùng một tham số đầu vào chúng ta nhận được cùng một kết quả trả về và các tham số truyền vào không bị thay đổi.

View sẽ lắng nghe Store, nếu có bất kỳ sự thay đổi nào, thì view sẽ cập nhật lại trạng thái. Việc áp dụng các nguyên lý trên tạo ra dòng dữ liệu một chiều (Unidirectional Data Flow) thay vì hai chiều như trong MVC. Trong thực tế, mô hình Redux sẽ sử dụng như hình sau:

angular-redux-30-638

Chúng ta cần thêm Action Creator và Middleware. Action Creator và Middleware sẽ được giới thiệu sâu hơn trong các phần tiếp theo, phần này chỉ giởi thiệu mục đích của nó.

Action Creator: trong các ứng dụng thực tế chúng ta cần thực hiện một hành động phức tạp như api, async, xử lý logic, … việc sử dụng một action cơ bản như vừa nêu trên không đáp ứng được các yêu cầu này. Action Creator sẽ xử lý việc này và phát ra các Action tương ứng để Store cập nhật lại kết quả

struct PostActionCreator {
    func refresh() -> (DispatchFunction, GetState) -> Void {
        return { dispatch, getState
            // xử lý logic
            api.getPosts() { posts in
                // cập nhật kết quả thông qua action
                dispatch(PostAction.updateResult(post))
            }
        }
    }
}

Middleware: nếu bạn nào đã từng viết ASP.Net Web API, có thể sẽ quen thuộc với ActionFilter. Về mục đích thì Middleware khá giống với mục đích của ActionFilter trong Web API. Mục đích chính thực hiện các yêu cầu phi chức năng, chẳng hạn: logging.

4. Lợi ích của Redux

  • Chúng ta không cần quan tâm tới việc đồng bộ hoá dữ liệu giừa các chức năng, thành phần trong hệ thống. Trạng thái của ứng dụng được lưu trữ tập trung, sự thay đổi trạng thái cũng được tập trung tại một điểm. Khi đó, store có thể dễ dàng phản hồi tới view.
  • Tách biệt vai trò và nhiệm vụ các thành phần.
  • Dễ dàng mở rộng, thêm mới chức năng
  • Dễ dàng thực hiện các tác vụ redo, undo
  • Dễ dàng debug
  • Unit-Test dễ dàng

Trong phần này mình đã giới thiệu các vấn mà chúng ta gặp phải khi sử dụng MVC , đồng thời mình cũng giới thiệu sơ lược về Redux. Khi mình mới tiếp cận Redux mình có hai câu hỏi mất khá nhiều thời gian để giải đáp:

  1. Việc lưu trữ trạng thái ứng dụng tại một điểm có làm ảnh hưởng tới bộ nhớ, performance của ứng dụng?
  2. Khi có một sự thay đổi nhỏ ở store, mọi tất cả các chức năng, thành phần lắng nghe tới nó sẽ nhận được phản hồi, dẫn tới việc render, hoặc trigger các tác vụ khác. Redux giải quyết bài toàn này như thế nào?

Hi vọng các phần tiếp theo có thể giải đáp được những thắc mắc này. Ngoài ra, các bạn có thể thấy rằng việc phản hồi của store cho các thành phần trong ứng dụng, mang tính reactive, nên có thể kết hợp sức mạnh của Reactive Programming cho phần này, do đó mình có viết lại framework của Benjamin Encz kết hợp với Reactive Programming. Các bạn có thể tham khảo trước tại đây

5 thoughts on “Giới thiệu Redux trong Swift

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s