Swift Automaton và ứng dụng trong chức năng đăng ký cho iOS app
Hi mọi người, lâu lâu chưa viết bài được nên cảm thấy bứt rứt kinh khủng Năm nay mình mới join Mercari, #1 unicorn startup của Japan nên mất tương đối thời gian làm quen với môi trường và cả cuộc sống mới. Tháng 10 vừa rồi cũng là đoạn khổ chiến để up sourcebase lên Swift 4 , RxSwift 4 và tối ...
Hi mọi người, lâu lâu chưa viết bài được nên cảm thấy bứt rứt kinh khủng Năm nay mình mới join Mercari, #1 unicorn startup của Japan nên mất tương đối thời gian làm quen với môi trường và cả cuộc sống mới. Tháng 10 vừa rồi cũng là đoạn khổ chiến để up sourcebase lên Swift 4, RxSwift 4 và tối ưu hoá cho iPhone X sắp ra mắt.
Như đã viết trong bài TIL trước, mình có phát biểu ở Tech Event về ứng dụng của Automaton (State Machine) trong lập trình iOS. Hôm nay sẽ là bài giải thích cụ thể ( hơn một chút ). Slide có thể xem dưới đây
Flow đăng ký và đăng nhập
Khi làm chức năng đăng ký và đăng nhập cho iOS app thì những phần cơ bản nhất bao gồm
- Đăng ký bằng email và password
- Đăng nhập bằng email và password
- Đăng ký và đăng nhập được bằng Facebook
- Khôi phục mật khẩu
Tuy vậy app mình làm Atte nằm trong 1 hệ sinh thái với app lớn hơn (Mercari) và muốn tiện cho người dùng thì còn cần
- Cho phép user của Mercari đăng ký / đăng nhập với thông tin có sẵn.
Trong app Atte có 2 loại token: Mercari Token và Atte token. Tất nhiên vì là token nên chỉ có hiệu lực trong 1 thời gian nhất định, và khi hết hạn thì cần bắn refresh api để "làm mới" token. Đây là tính năng cơ bản của các loại token nói chung. Người dùng đang cài app Mercari thì sẽ có Mercari Token, đang cài cả Mercari và Atte thì sẽ có cả 2 loại token nêu trên
Khởi động app lần đầu
Vần đề đầu tiên: khi người dùng khởi động app lên, tuỳ xem trong iPhone có sẵn loại token nào mà phải hiển thị 3 màn hình khác nhau
- Màn hình đăng ký mới từ đầu
- Màn hình đăng ký từ tài khoản Mercari
- Màn hình one-click-login (đăng nhập trong 1 nốt nhạc)
Đăng ký bằng Facebook
Khi người dùng khởi động app và hoàn toàn chưa có 1 token nào, thì có thật sự người dùng là mới toanh đối với app của chúng ta hay không ?
Câu trả lời là, chỉ khi anh ta dùng 1 cái iPhone duy nhất thì điều trên mới đúng.
Anh người dùng của chúng ta hoàn toàn có thể mới tậu iPhone X rồi tự dưng quên sạch ko chuyển dữ liệu sang rồi tải app lại từ đầu. Ảnh có thể vốn là Android fanboy và mới thử turn iPhone đầu tiên và (tất nhiên) cũng mới tải app hôm nay. Lúc này nếu tài khoản cũ có nối với tài khoản Facebook, và anh ấn nút "Login bằng Facebook" trong app thì cũng phải tuỳ theo 3 trường hợp và đẩy ra 3 màn hình khác nhau hệt như bên trên
Đăng nhập bằng Email
Đăng nhập bằng email cũng xảy ra case tương tự. Tuỳ vào tài khoản đã được lập, đã lập bằng app chính hay hoàn toàn mới mà phải rẽ nhánh màn hình. Đồng thời ở màn hình đăng nhập email cũng có nút Facebook. Và nếu bạn đã làm việc với Facebook SDK thì bạn sẽ biết nút này không khác gì nút Facebook của đăng ký cả. Điều đó có nghĩa là ấn vào nút này xong cũng phải rẽ 3 nhánh tiếp như bên trên.
Logic rẽ nhánh là Domain knowledge
Khi flow càng ngày càng phức tạp, nếu không có cách quản lý logic rẽ nhánh tốt thì sẽ dẫn tới code xoắn quẩy rất nhanh. Đồng thời khi phát sinh yêu cầu rẽ nhánh mới thì chuyện đi tìm đúng chỗ cũng phát mệt. Hơn nữa màn hình nào chuyển sang màn hình nào là Business Logic, theo quan điểm của Domain Driven Design thì là Domain Knowledge, cần để trong tầng Model và cần phải test được mà không chịu ảnh hưởng của UI.
Mỗi khí rẽ nhánh thay đổi thì parameter (gọi là dependency) cho màn hình đích đến cũng có thể thay đổi. Dependency mà hơi tí thay đổi thì không thể Move Fast được
Đến đây mình sẽ giới thiệu cách ứng dụng Automaton (State Machine) để giải quyết bài toán trên.
Hàm chuyển đổi của State Machine
Nói 1 cách đơn giản thì State Machine là kỹ thuật quản lý State (trạng thái) trong mô hình 1 chiếc máy tính. Trong giới FrontEnd có 2 kiến trúc rất nổi tiếng là Redux và Elm, mà bản chất bên trong đều là State Machine.
Nếu bạn đọc Automata Theory thì bạn sẽ biết có rất nhiều loại State Machine khác nhau: Mealy Machine, Moore Machine, Turing Machine. Điểm khác biệt chủ yếu của những "máy" này là hàm chuyển đổi, hay còn có 1 cái tên khác là Reducer. Nếu xem s là trạng thái, a là input và b là output thì công thức hàm chuyển đổi của FSM, Mealy Machine và Moore Machine là như sau
// FSM (s, a) -> s // Mealy Machine (s, a) -> s (s, a) -> b // Moore Machine (s, a) -> s s -> b
Nếu bạn biết Elm thì sẽ thấy hàm của Mealy Machine rất giống Elm. Nếu áp dụng thêm chuyển đổi curry function thì có thể dẫn tiếp đến Stateful Computation, hay còn gọi là State Monad
// Elm fun update(state: State, action: Action): Pair<State, Command> // Mealy Machine (s, a) -> (s, b) // aka a -> s -> (s, b) // aka a -> State[s, b]
Với hàm a -> State[s, b] ở cuối, mình có thể implement một State Machine rất đơn giản như sau
class MonadicAutomaton<S, A, B> { typealias T = (A) -> State<S, B> private var f : T init(f: @escaping T) { self.f = f } func transition(from: S, by: A) -> (S, B) { return f(by).run(s: from) } }
Input của Automaton là Event
A ở bên trên là sự kiện gây ra chuyển đổi trạng thái. Sự kiện này có thể định nghĩa bằng enum với associated values, dùng để chưa thông tin cần thiết cho lần chuyển đổi trạng thái, hay cụ thể hơn là dependency cho ViewController của màn hình tiếp theo.
Ở đây mình muốn nói thêm 1 chút về dependency của ViewController. Đây là 1 trong các policy của team, cụ thể là không sử dụng Segue trên storyboard để thực hiện chuyển màn hình. Tất cả các ViewController đều định nghĩa Dependency cụ thể và conform theo 1 protocol gọi là DependencyInjectable
protocol DependencyInjectable { associatedtype Dependency = Void static func make(withDependency dependency: Dependency) -> Self } final class AtteLoginViewController: UIViewController, DependencyInjectable { struct Dependency { let resource: MercariIdResource let token: String } private var resource: MercariIdResource! private var token: String! static func make(withDependency dependency: Dependency) -> AtteLoginViewController { let vc = AtteLoginViewController() vc.resource = dependency.resource vc.token = dependency.token return vc } }
Tất cả thay đổi UI đều là Effect của State Machine
Khi State Machine thay đổi trạng thái bên trong thì các ảnh hưởng phát tán ra app chủ yếu đều là các thay đổi về UI.
Lần này State Machine là phạm vi 1 app, nên có thể coi ouput là cả ViewController, hay nói cách khác B = UIViewController. Ngược lại nếu bạn tạo 1 State Machine trong phạm vi bên trong 1 màn hình thì output sẽ là sự thay đổi hình thái của các nút, ảnh nền hay bất cứ chuyển động nào trên màn hình đó. Nếu dùng RxSwift thì mình nghĩa B = Disposables
Khi chỉ định B là UIViewController thì hàm chuyển đổi của mình sẽ trông như sau
let transitionFunc: (REvent) -> State<RState, UIViewController> = { event in switch event { case .registerWithFacebook(let profile): let vc = ProfileRegisterViewController.make(withDependency: .init(facebookProfile: profile)) return State<RState, UIViewController> { s in let s1: RState = s == .registerRoot ? .profileRegister : .any return (s1, vc) //... } } let registrationMachine = MonadicAutomaton<RState, REvent, UIViewController>(f : transitionFunc)
Lời kết
Nếu bạn nào muốn đọc code thì có thể xem lại Slide ở đầu bài hoặc xem ở demo của mình:
https://github.com/orakaro/MonadicMealyMachine
Ngoài ra, trong try! Swift NewYork năm nay anh @kzaher, tác giả của RxSwift có giời thiệu một State Machine trong thư viện RxFeedback rất hay. Mặc dù Reducer của RxFeedback vẫn dùng switch nhưng nguyên hàm chuyển đồi được ném vào toán tử scan của RxSwift để tạo ra stream của State, và Event tương ứng, rất tuyệt.
https://academy.realm.io/posts/try-swift-nyc-2017-krunoslav-zaher-modern-rxswift-architectures/
Cuối cùng, có một bài talk về Composable Reducer rất ấn tượng trong Functional Swift Conference 2017
Composable Reducers & Effects Systems