iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 28
0
Software Development

Functional Programming in Kotlin系列 第 28

Functional Programming in Practice - Part 2

Side effect operator

使用了 RxJava 之後,並沒有讓這邊的程式碼變得更複雜。但是,這邊有一件事需要被探討,在 LoginRepositorylogin 有一個 side effect operator - doOnSuccess 。我們允許在這邊有 side effect 嗎?為什麼呢?

在之前的篇幅中,我們提到了在 functional programming 我們不喜歡 side effect ,因為他代表了意外,代表了隱含(implicity)的效果。 Pure function 是應該要被遵循的規則,然而在這裏我們破壞了這個規則,這樣對嗎?但其實,我們可以看待 LoginRepository 這個整體就是一個“容器",一個有狀態的"容器",在 functional 的世界裡不會有 LoginRepository 的實作,他是被排除在外的,實作是在物件導向的世界中,所以這邊就有一個非常重要的結論:在一個專案裡, functional programming 跟 object oriented programming 應該是要可以並存的,但我們要劃清界線,搞清楚什麼類別應該要 functional ,什麼類別應該要 object oriented,而且很多時候寫 object oriented 會比 functional 簡單、直覺得多。

https://user-images.githubusercontent.com/7949400/94897214-be218880-04c1-11eb-9bef-ba970b17b069.png

ViewModel

接下來看看 ViewModel 要改成怎樣,ViewModel 有兩個不同的 LiveData ,其中 LoginResult 是用來顯示結果,另一個 LoginFormState 用來即時顯示輸入的回饋,像是帳號空白、密碼長度不足等等。但是剛剛在 LoginRepository 已經做了很多修改,這邊也要做相對應的修正才行。

首先是登入,由於LoginRepository 已經將介面修改為 Single<LoggedInUser> ,原來的版本是 Result<LoggedInUser> ,所以要改成非同步的處理方式:

class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {

		private val _loginResult = MutableLiveData<LoginState>()
		val loginResult: LiveData<LoginState> = _loginResult

    private val disposables = CompositeDisposable()
		
		fun login(username: String, password: String) {
		    loginRepository.login(username, password)
		        .subscribe( { user ->
		            loginResult.postValue(LoginState.Success(user.displayName))
		        }, {
		            loginResult.postValue(LoginState.Failed(R.string.login_failed))
		        })
		        .addTo(disposables)
		}
}

RxJava 標準的使用方式是 subscribe 來接收結果,另外,由於 login 是一個背景任務,所以要使用 postValue 來更新資料。接下來看看 LoginActivity 是怎麼接收資料的:

class LoginActivity : AppCompatActivity() {

		override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

				loginViewModel.loginResult.observe(this@LoginActivity, Observer {
				    val loginResult = it ?: return@Observer
				
				    loading.visibility = View.GONE
				    if (loginResult.error != null) {
				        showLoginFailed(loginResult.error)
				    }
				    if (loginResult.success != null) {
				        updateUiWithUser(loginResult.success)
				    }
				    
				})
		}
}

看吧!如果是 Product Type 再加上 null 的話,就會有這種這種奇怪的 null check 程式碼,就算我們都知道,不可能 error 跟 success 同時都是 null 或是同時都不是 null 。在這邊的程式碼卻不是我們想像中的那樣,可以用一個簡單的 if 或是 when 來完成,還有我們還發現了一個 loading 的狀態,照理說,這個狀態應該要由 ViewModel 來控制,所以我們再把它加入到 LoginResult 的狀態中,並重新命名為 LoginProgress

class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {

    // 簡化成只使用一個 LiveData
    val loginProgress: MutableLiveData<LoginProgress> = MutableLiveData<LoginProgress>()

    private val disposables = CompositeDisposable()

    fun login(username: String, password: String) {
        loginProgress.postValue(LoginProgress.Loading)
        loginRepository.login(username, password)
            .subscribe( { user ->
                loginProgress.postValue(LoginProgress.Success(user.displayName))
            }, {
                loginProgress.postValue(LoginProgress.Failed(R.string.login_failed))
            })
            .addTo(disposables)
    }
}

class LoginActivity : AppCompatActivity() {

    private lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        loginViewModel.loginProgress.observe(this@LoginActivity, Observer {
            val loginResult = it ?: return@Observer

            when (loginResult) {
                is LoginProgress.Success -> {
                    loading.visibility = View.GONE
                    updateUiWithUser(loginResult.displayName)
                }
                is LoginProgress.Failed -> {
                    loading.visibility = View.GONE
                    showLoginFailed(loginResult.errorMsg)
                }
                is LoginProgress.Loading -> {
                    loading.visibility = View.VISIBLE
                }
            }
        })

修改過後,在 LoginActivityloginProgress.observe 從原本的兩個 null check ,變成了單一的 when ,很好的控制了所有的狀態,不用擔心因為修改程式碼而使得不可能的狀態出現(兩個 null 或是兩個都不是 null)。


上一篇
Functional Programming in Practice - Part 1
下一篇
Functional Programming in Practice - Part 3
系列文
Functional Programming in Kotlin30

尚未有邦友留言

立即登入留言