0

Giới thiệu về kiến trúc Viper.

Chắc hẳn các bạn lập trình viên iOS đã rất quen thuộc với mô hình MVC được áp dụng trong iOS. Tuy nhiên, khái niệm ViewController của iOS thực sự biến việc phát triển các ứng dụng lớn, với chức năng màn hình phức tạp trở thành một mớ .... (you know what i mean). Trải qua quá trình phát triển, bảo trì, thêm thắt chức năng, ViewController dần thay đổi để trở thành một khái niệm mới : Massive ViewController với số lượng dòng code có thể lên tới hàng nghìn dòng trong một file. Và kể cả bạn có tách class tốt thế nào đi chăng nữa, việc một lập trình viên có vinh hạnh đi hót .... cho bạn sau đó cũng trở nên thực sự bốc mùi đối với họ.

Sau vài ngày nghiên cứu, tôi đã tìm thấy thứ này trên mạng. Họ gọi nó là VIPER - Một Clean Architecture cho các ứng dụng iOS. Áp dụng VIPER vào ứng dụng mới nhất tôi đang phát triển cho Framgia, hiệu quả của nó khiến tôi thấy ổn và quyết định sẽ giới thiệu VIPER tới mọi người qua bài viết này.

I.VIPER

VIPER bao gồm : VIEW - INTERACTOR - PRESENTER - ENTITY - ROUTING 2014-06-07-viper-intro-0a53d9f8.jpg

VIEW : hiển thị giao diện dựa trên hướng dẫn của PRESENTER đồng thời tiếp nhận input và truyền tới PRESENTER

INTERACTOR : Chứa các business logic tùy theo use case tương ứng.

PRESENTOR : Chứa view logic để hiển thị ( nhận kết quả từ interactor), nhận user input và request tới interactor.

ENTITY : Các model object được sử dụng bởi interactor

_ ROUTING : điều khiển việc hiển thị các màn hình. _

2014-06-07-viper-wireframe-76305b6d.png

Với Viper, chúng ta sẽ chia project thành các module tương ứng với từng use case của ứng dụng , mỗi module sẽ bao gồm các layer tương ứng như trên. Để các bạn dễ hiểu, phần tiếp theo mình sẽ xây dựng một ứng dụng đơn giản bám sát với việc xây dựng từng layer trong VIPER.

Ứng dụng của mình sẽ chỉ có một use case đơn giản : Tiến hành quá trình login

INTERACTOR:

Đầu tiên, ta sẽ define ra 2 protocol input, out put làm nhiệm vụ liên lạc giữa presenter và interactor.

protocol LoginInput {
    func loginInput(userName : String?, password : String?)
}
protocol LoginOutput {
    func loginOutput(result : Bool, message : String)
}

Interactor chứa bussiness logic , công việc thực hiện trên Interactor hoàn toàn không liên quan gì tới UI, và chỉ tương tác với dữ liệu. Do đó, chúng ta cũng có thể dễ dàng áp dụng TDD ( Test Driven Development) đối với việc sử dụng VIPER.

Ở đây, Interactor nhận username và password từ presenter, so sánh dữ liệu với DataStore và trả về kết quả tương ứng.

func loginInput(userName: String?, password: String?) {
        if userName == nil {
            output?.loginOutput(false, message: "Please tell me your user name!!!!")
            return
        }
        if password == nil {
            output?.loginOutput(false, message: "Please tell me your password!!!!")
            return
        }
        if userName != DataStore.sharedInstance.user.userName {
            output?.loginOutput(false, message: "User name is not correct")
            return
        }
        if password != DataStore.sharedInstance.user.password {
            output?.loginOutput(false, message: "Password is not correct")
            return
        }
        output?.loginOutput(true, message: "Correct!!!!")
    }

ENTITY

Entity chỉ được thao tác bởi Interactor, ở đây, tôi chỉ tạo ra object UserEntity và sử dụng class DataStore để lưu trữ dữ liệu tạm, tuy nhiên việc thao tác với các cơ sở dữ liệu khác như CoreData hay Sqlite cũng k có nhiều khác biệt.

PRESENTER

Trái ngược với Interactor, Presenter chủ yếu chứa các logic để điều khiển UI. Presenter nhận Input từ các interactor và update UI, đồng thời gửi request tới các interactor.

class LoginPresenter: NSObject, LoginEventHandler {

    var input : LoginInput?
    var loginVC : LoginViewController?

    func login(userName: String?, password: String?) {
        input?.loginInput(userName, password: password)
    }

}

extension LoginPresenter : LoginOutput {

    func loginOutput(result: Bool, message: String) {
        if result {
            // move to next screen
            LoginWireFrame.sharedInstance.presentSuccessViewController()
        } else {
            LoginWireFrame.sharedInstance.presentAlertForLoginView(message)
        }
    }

}

Chẳng hạn trong trường hợp này, class LoginPresenter nhận thông tin từ LoginViewController thông qua delegate LoginEventHandler, và request tới Interactor bằng LoginInput. Sau khi Interactor xử lí thông tin, dữ liệu lại được update ngược tới LoginViewController bằng LoginOutput.

View

Trong trường hợp này chính là các ViewController, nhưng chúng bị động, chỉ chờ để nhận lệnh xử lí từ Presenter. Nếu như trong kiến trúc MVC quen thuộc ta vẫn làm, ViewController chứa rất nhiều logic xử lí giao diện, đôi khi cả dữ liệu thì giờ đây các phần công việc đó đều đã được Presenter và Interactor xử lý.

ROUTING

Routing chính là lớp LoginWireFrame, layer này chỉ thực hiện khởi tạo các màn hình, và điều khiển việc hiển thị, dịch chuyển các màn hình.

class LoginWireFrame: NSObject {

    var loginViewController : LoginViewController?

    class var sharedInstance : LoginWireFrame {
        struct Static {
            static let instance : LoginWireFrame = LoginWireFrame()
        }
        return Static.instance
    }

    func presentLoginViewControllerFromWindow(window : UIWindow) {
        // create LoginViewController instance
        let storyBoard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
        let loginVC = storyBoard.instantiateViewControllerWithIdentifier("LoginViewController")
            as! LoginViewController

        // create presenter && interactor
        let loginPresenter = LoginPresenter()
        let loginInteractor = LoginInteractor()
        loginPresenter.input = loginInteractor
        loginPresenter.loginVC = loginViewController
        loginInteractor.output = loginPresenter

        // set event handler for loginViewController
        loginVC.eventHandler = loginPresenter
        self.loginViewController = loginVC
        let navController = UINavigationController(rootViewController: self.loginViewController!)
        window.rootViewController = navController
    }

    func presentSuccessViewController() {
        let storyBoard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
        let successViewController = storyBoard.instantiateViewControllerWithIdentifier("SuccessViewController")
            as! SuccessViewController
        dispatch_async(dispatch_get_main_queue()) { () -> Void in
            self.loginViewController?.navigationController?.pushViewController(
                successViewController,
                animated: true
            )
        }
    }

    func presentAlertForLoginView(message : String) {
        // show alert
        let alertViewController = UIAlertController(
            title: "Login Failed",
            message: message,
            preferredStyle: .Alert
        )

        let okAction = UIAlertAction(
            title: "OK",
            style: .Cancel,
            handler: { (action : UIAlertAction) -> Void in
                print("OK")
        })

        alertViewController.addAction(okAction)
        loginViewController?.presentViewController(alertViewController, animated: true, completion: nil)
    }

}

Kết luận :

Mặc dù project được sử dụng kiến trúc Viper có nhược điểm là nó chứa nhiều class hơn so với cách thông thường. Tuy nhiên giờ đây bạn sẽ không còn gặp phải các class quá lớn và phức tạp như trước kia. Ứng dụng trở nên mạch lạc hơn do các use case được chia ra rõ ràng. Điều quan trọng hơn là ta có thể dễ dàng viết Test hoặc triển khai TDD.

Các bạn có thể tải sample mẫu ở đây :

https://drive.google.com/file/d/0B9FOeas3cAbIeXRnTUVPUmZUaUE/view

Chúc các bạn vui vẻ.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.