iOS · reactive programming · ReactiveX

Giới thiệu Reactive Programming trong RxSwift – Phần 1

Có lần một tay lão luyện trong team giới thiệu những thứ có thể làm với ReactiveX. Lão này chỉ giới thiệu về những thứ mà ReactiveX hỗ trợ, chứ không đề cập nhiều tới chi tiết, mặc dù lúc đó mọi thứ còn rất mơ hồ, nhưng mình cực kỳ hứng thú với cái này vì những lợi ích của nó mang lại cho dự án hiện tại nên cố gắng nghiên cứu tìm hiểu thực hư thế nào. Khi tìm hiểu mình gặp không ít khó khăn trong việc tìm kiếm tài liệu, nên mình quyết định viết một số bài về Functional Reactive Programming và thư viện hiện thực cho FRP là ReactiveX.

Phần 1: Giới thiệu Functional Reactive Programming
Phần 2: Sử dụng ReactiveX trong Swift
Phần 3: Lập trình bất đồng bộ, song song và quản lý bộ nhớ

Functional Reactive Programming là gì?

Sau khi tìm hiểu khá nhiều nguồn khác nhau, mình tìm thấy định nghĩa của Andre Staltz là dễ hiểu nhất “Reactive programming is programming with asynchronous data streams (Xem thêm: The introduction to Reactive Programming you’ve been missing). Ở khái niệm trên, có hai điểm quan trọng chúng ta cần phân tích là StreamAsynchronous

Stream:  Khi thực hiện một tác vụ bất kỳ, chúng ta chỉ quan tâm tới 3 yếu tố: giá trị trả về từ tác vụ đó (data), lỗi (error – nếu có) và thời điểm tác vụ đó kết thúc (completed signed). Khi lập trình đồng bộ (synchronous) việc xác định 3 yếu tố này không khó khăn gì, nhưng khi lập trình bất đồng bộ(asynchronous) việc xác định các yếu tố này không dễ dàng (đặc biệt là trong trường hợp thông qua nhiều lớp). Như vậy, ta thấy cần có một cơ chế giúp xác định các yếu tố này trong cả lập trình bất đồng bộ cũng như đồng bộ. FRP giải quyết vấn đề này bằng cách sử dụng stream để truyền tải: dữ liệu trả về, lỗi và tín hiệu kết thúc của một tác vụ  theo trình tự thời gian từ nơi phát ra tín hiệu (Producer) tới nơi lắng nghe (Subscriber).

687474703a2f2f692e696d6775722e636f6d2f634c344d4f73532e706e67

(Hình: Andre Staltz)

Thoạt đầu chúng ta thấy nó khá giống với callback, event-based programs, hoặc promise, không có gì mới. Nhưng nếu để ý sẽ thấy callback sẽ trở nên cực kỳ phức tạp nếu sử dụng nhiều cấp (nested callback). Nếu so sánh với promise thì thấy sự khác biệt duy nhất là stream của FRP có thể nhả ra nhiều giá trị theo dòng thời gian, trong khi đó promise trả về một giá trị duy nhất (ngoài error và completed signal).

Nếu chỉ dừng lại ở đó thì FRP cũng không có gì đặc biệt. Điều tạo ra sức mạnh của FRP là việc áp dụng functional programming cho phép filter (filter, take, scan, …), chuyển đổi từ stream này qua stream khác (map, flatMap, reduce), hoặc merge nhiều stream thành một stream mới (combine, merge, zip, …) khá dễ dàng mà không làm thay đổi trạng thái của stream ban đầu. Ngoài ra trong FRP, tất cả mọi thứ có thể được coi là stream, việc áp dụng functional programming giúp cho việc khởi tạo stream một cách dễ dàng. Hình bên dưới cho minh hoạ cho quá trình chuyển đổi stream. Từ một stream ban đầu (trên cùng), có thể được chuyển đổi thành nhiều streams phía dưới, stream ban đầu không bị thay đổi.

frp_transform

(Hình: Andre Staltz)

Ví dụ: Swift code bên dưới thực hiện cho yêu cầu trong hình ở trên: Trong khoảng 1/4 giây, nếu người dùng nhấn button từ 2 lần trở lên thì print ra số lần nhấn. Đoạn code thực hiện 2 lần chuyển đổi qua stream mới: buffer và map, 1 lần filter. Buffer được chạy ở background thread, tránh bị block UI. Trong phần này mình sẽ không đi quá chi tiết vào việc hiện thực, chỉ nêu ví dụ để các bạn có thể thấy được sức mạnh của FRP.

button.rx_tap
      .buffer(timeSpan: 0.25, count: 3, scheduler: backgroundDefault)
      .map { $0.count }
      .filter { $0 >= 2 }
      .subscribeNext { count in
          print(count)
       }.addDisposableTo(disposeBag)

Asynchronous: Trong phần định nghĩa của  Andre Staltz ở trên có đề cập tới asynchronous. Chính điều này gây ra sự hiểu nhầm ReactiveX là một thư việc lập trình bất đồng bộ, chỉ cần sử dụng ReactiveX sẽ đảo bảo ứng dụng được lập trình asynchronous, điều này không chính xác, nên dùng “dealing with asynchronous programming” thì hợp lý hơn. Hãy xem ví dụ sau:

final class FirstViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let observable = Service().count()
        observable.subscribe(onNext: { number in
            print("Receive item #\(number) on thread \(Thread.current)")
        }).addDisposableTo(disposeBag)
        print("viewDidLoad complete on thread \(Thread.current)")
    }
    let disposeBag = DisposeBag()
}

final class Service {
    func count() -> Observable {
        return Observable.create { o in
            print("Emit first item on thread \(Thread.current)")
            o.onNext(1)
            print("Going to sleep")
            Thread.sleep(forTimeInterval: 5)
            print("Emit second item on thread \(Thread.current)")
            o.onNext(2)
            o.onCompleted()
            return Disposables.create()
        }
    }
}

Kết quả sau cho thấy tất cả mọi tác vụ được thực thi tuần tự trong cùng một thread, không hề có bất đồng bộ ở trong ví dụ này.

Emit first item on thread (id = 0x600000073900, name = main)
Receive item #1 on thread (id = 0x600000073900, name = main)
Going to sleep
Emit second item on thread (id = 0x600000073900, name = main)
Receive item #2 on thread (id = 0x600000073900, name = main)
viewDidLoad complete on thread (id = 0x600000073900, name = main)

Mặc định thì mọi tác vụ sẽ được thực thi tại thread của subscriber, ReactiveX có cung cấp phương thức (observeOn) để điều hướng stream trên các thread khác nhau, điều này làm tăng thêm sức mạnh của FRP, nhưng đó không phải mục đích chính của FRP. Mục tiêu chính của FRP là cung cấp cho chúng ta một phương tiện hữu hiệu để tương tác giữa các tác vụ bất đồng bộ, giúp các bên trong phần mềm có thể giao tiếp mà không cần quan tâm các vấn đề như: concurrence, thread-safety, error handling, …

Tại sao sử dụng Reactive Programming

Lợi ích đầu tiên là giúp cho việc lập trình bất đồng độ trở nên dễ dàng, nhanh chóng, lập trình viên không quan tâm tới thread safety, concurrency, error handling, …

Lợi ích tiếp theo là giúp hạn chế lưu trữ, quản lý các state trung gian. Trong ví dụ clickStream trên, nếu như sử dụng cách lập trình thông thường, thì phải khai báo rất nhiều biến (state) để lưu trữ các bước trung gian. Ví dụ: timer,  click count collection, … Trong FRP, các bước này là không cần thiết nhờ khả năng chuyển đổi stream (map, flatMap, reduce, ….).

RP giúp giảm thiểu coupling thông qua việc tách biệt producer and cosumer. Producer xử lý và nhả ra các events theo một stream. Consumer lắng nghe và xử lý dựa trên các event trong stream đó. Hai bên không cần quan tâm tới sự thực hiện bên trong của mỗi bên.

Một điểm mạnh khác của RP là giúp cho việc xử lý lỗi trong lập trình bất đồng bộ nhẹ nhàng hơn rất nhiều. Nếu bạn nào từng handle error khi lập trình bất đồng bộ, multiple thread, thì sẽ thấy việc này không hề dễ dàng. RP giúp tách biệt việc xử lý lỗi với logic. Việc này giúp cho code trong sáng hơn rất nhiều. Trong ví dụ trên, nếu bạn muốn thêm phần xử lý lỗi, chỉ cần thêm onError:

let observable = Service().count()
observable.subscribe(onNext: { number in
    print("Receive item #\(number) on thread \(Thread.current)")
}, onError: { error in
    print(error)
}).addDisposableTo(disposeBag)

Ngoài ra các bên trung gian có thể lắng nghe và xử lý side-effects khá dễ dàng. Như thế nào là side-effects và cách xử lý nó sẽ được nói cụ thể trong phần tiếp theo. Giả sử, ở đây có một lớp trung gian giữa Service và ViewController là ViewModel, mỗi lần gọi hàm count thì biến totalCall sẽ được tăng thêm 1. Ta sẽ thực hiện như sau (hàm count() chỉ được thực thi khi ViewController subscribe tới nó, cùng lúc đó nó sẽ thực thi phần được định nghĩa trong doOnNext):

final class ViewModel {
    func count() -> Observable {
        return Service().count().do(onNext: {
           totalCall += 1
        })
    }
    var totalCall = 0
}
let observable = ViewModel().count()
observable.subscribe(onNext: { number in
    print("Receive item #\(number) on thread \(Thread.current)")
}).addDisposableTo(disposeBag)

Một lưu ý là hiện tại thư viện ReactiveX đã support nhiều ngôn ngữ khác nhau như C#, Swift, Java, JavaScript, … Angular JS 2, sử dụng RxJS

Kết

Hi vọng bài biết cung cấp khái niệm cơ bản về reactive programming, asynchronous và streams trong FRP. Sự khác biệt của FRP với callback, promise và những điểm mạnh của reactive programming. Một điều ngoài luồng mình cũng muốn đề cập thêm là khi nghiên cứu một công nghệ bất ký thì việc đưa ra kết luận của bản thân mình là điều tối quan trọng, nếu không chúng ta chỉ là một cỗ máy. Bài viết này là cái kết của bản thân mình về FRP. Do tài liệu không  nhiều nên mình áp dụng cách sau để ra kết luận: Tìm hiểu -> đưa giả định -> kiểm chứng -> sai -> đưa giả định mới -> kiểm chứng, … -> Kết luận.

Advertisements

2 thoughts on “Giới thiệu Reactive Programming trong RxSwift – Phần 1

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 )

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 )

Google+ photo

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

Connecting to %s