[iOS] 条件分岐を減らして、処理をシンプルにする

# はじめに
①ViewControllerから処理の一部を切り出したい　　→　ViewControllerの肥大化
②ViewControllerで状態（フラグ）を意識したくない　→　フラグ管理がカオス
③ユニットテストしやすくしたい　→　テスト容易性

上記の課題を解消する一つの方法をご紹介します。

その答えは、「条件分岐を減らす」です。　

## 環境
XCode : 8.0
Swift : 3.0

# 今回ご説明する例
今回は、下記のようなシンプルな例で説明します。

①ログオフ状態の場合、ログインボタンを表示する（フォントカラーは青色）
②ログイン状態の場合、ログアウトボタンを表示する（フォントカラーは赤色）

# 1. Badケース
まず最初にBadケースをご紹介します。

ViewController内でログイン状態を意識し、
ログインとログオフで処理や表示に違いがある度に、
条件分岐が増えてしまいます。
ユニットテストの実施も難しいですね。

※今回は、UI周り(storyboard関連）は割愛させていただきます。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var loginButton: UIButton!
    private var isLogined = false
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLoginButton()
    }
    
    //MARK:- IBAction
    @IBAction func didTapLogin(_ sender: UIButton) {
        
        isLogined = loadLoginState()
        
        if isLogined {
            setLogoutState()
        } else {
            setLoginState()
        }
        setupLoginButton()
    }
    
    //MAKR:- Private
    fileprivate func setupLoginButton() {
        
        isLogined = loadLoginState()

        if isLogined {
            setLoginButtonView()
        } else {
            setLogoutButtonView()
        }
    }
    
    fileprivate func setLoginState() {
        UserDefaults.standard.set(true, forKey: "isLogined")
        UserDefaults.standard.synchronize()
    }
    
    fileprivate func setLogoutState() {
        UserDefaults.standard.set(false, forKey: "isLogined")
        UserDefaults.standard.synchronize()
    }
    
    fileprivate func loadLoginState() -> Bool{
        return UserDefaults.standard.bool(forKey: "isLogined")
    }
    
    fileprivate func setLogoutButtonView() {
        loginButton.setTitle("ログアウト", for: .normal)
        loginButton.setTitleColor(UIColor.red, for: .normal)
    }
    
    fileprivate func setLoginButtonView() {
        loginButton.setTitle("ログイン", for: .normal)
        loginButton.setTitleColor(UIColor.blue, for: .normal)
    }
}
```

# 2. Goodケース
今回の課題を解決するために、
[GoFのStrategy パターン](https://www.techscore.com/tech/DesignPattern/Strategy.html/) or Stateパターンを利用して、条件分岐を減らします。
それでは、やってみます。

## 2.1. ログイン状態を管理するクラス
ログイン状態は、UserDefaultsにて管理します。

必要な機能は、下記の3点とします。
①ログイン中かどうか？
②ログインする
③ログアウトする

### 2.1.1. プロトコル

```swift:LoginStateRepository.swift
import UIKit

protocol LoginStateProtocol {
    func isLogined() -> Bool
    func setLoginState()
    func setLogoutState()
}
```
### 2.1.2. UserDefaultsのキー名

```swift:LoginStateRepository.swift
struct LoginStateUDConst {
    static let isLogined = "isLogined"
}
```

### 2.1.3. 実装クラス

```swift:LoginStateRepository.swift
final class LoginStateRepository: LoginStateProtocol {
    
    func isLogined() -> Bool {
        return UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined)
    }
    
    func setLoginState() {
        UserDefaults.standard.set(true, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()
    }
    
    func setLogoutState() {
        UserDefaults.standard.set(false, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()        
    }
}

```
## 2.2. ログイン・ログアウトの実行、ログインボタンの表示切り替えを行うクラス

### 2.2.1. プロトコル

必要な機能は、下記の3点とします。
①ログインまたは、ログアウトする
②ログインボタンのタイトルを取得する
③ログインボタンのフォントカラーを取得する

```swift:LoginUsecase.swift
import UIKit

protocol LoginUsecaseProtocol {
    func loginOrLogout()
    func loginButtonTitle() -> String
    func loginButtonTitleColor() -> UIColor
}
```

### 2.2.2. ログインボタンのタイトル

```swift:LoginUsecase.swift
struct LoginTitleString {
    static let login = "ログイン"
    static let logout = "ログアウト"
}
```

### 2.2.3. ログイン用の実装クラス
ログイン時の処理を記載します

```swift:Login.swift
import UIKit

final class Login: LoginUsecaseProtocol {
    
    func loginOrLogout() {
        LoginStateRepository().setLoginState()
    }
    
    func loginButtonTitle() -> String {
        return LoginTitleString.login
    }
    
    func loginButtonTitleColor() -> UIColor {
        return UIColor.blue
    }
}

```

### 2.2.4. ログアウト用の実装クラス
ログアウト時の処理を記載します

```swift:Logout.swift
import UIKit

final class Logout: LoginUsecaseProtocol {
    
    func loginOrLogout() {
        LoginStateRepository().setLogoutState()
    }
    
    func loginButtonTitle() -> String {
        return LoginTitleString.logout
    }
    
    func loginButtonTitleColor() -> UIColor {
        return UIColor.red
    }
}
```
### 2.2.5. ViewControllerからのディスパッチするクラス
ここで初めて、分岐処理が登場します。

ログイン中は、ログアウトインスタンスを、
ログアウト中は、ログインインスタンスを返却します。

それ以外の処理は、ViewControllerの指示の元に、該当インスタンスへ処理を委譲します。（ポリモーフィズム）

```swift:LoginUsecase.swift
final class LoginUsecase {
    
    fileprivate var loginState: LoginUsecaseProtocol?
    
    init() {
        loginState = LoginStateRepository().isLogined() ? Logout() : Login()
    }
    
    func loginOrLogout() {
        loginState?.loginOrLogout()
    }
    
    func loginButtonTitle() -> String {
        return loginState?.loginButtonTitle() ?? ""
    }
    
    func loginButtonTitleColor() -> UIColor {
        return loginState?.loginButtonTitleColor() ?? UIColor.black
    }
}

```
## 2.3. ViewControllerクラス
初期表示とボタン押下時の処理を実装します
基本的には、ViewController内では、
ログインの状態を管理せずに、ユースケースに処理を委譲しています。

※今回は、UI周り(storyboard関連）は割愛させていただきます。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var loginButton: UIButton!
    
    //MARK:- LifeCycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupLoginButton()
    }
    
    //MARK:- IBAction
    @IBAction func didTapLogin(_ sender: UIButton) {
        login()
        setupLoginButton()
    }
    
    //MAKR:- Private
    fileprivate func setupLoginButton() {
        let loginUsecase = LoginUsecase()
        loginButton.setTitle(loginUsecase.loginButtonTitle(), for: .normal)
        loginButton.setTitleColor(loginUsecase.loginButtonTitleColor(), for: .normal)
    }
    
    fileprivate func login() {
        LoginUsecase().loginOrLogout()
    }
}

```

# まとめ
・ViewControllerから処理の一部を切り出したい
・ViewControllerで状態を意識したくない
・テストしやすくしたい

上記の課題は、一応クリアできたのではないでしょうか。
もっと、良い方法があれば、ご教授頂ければ幸いです。

おい、ユニットテストはどうなったという方のために、おまけ

## おまけ

```swift:LoginUsecaseTests.swift
import XCTest
@testable import Test

class LoginUsecaseTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        
        //デフォルトは、false（ログアウト状態）
        UserDefaults.standard.set(false, forKey: LoginStateUDConst.isLogined)
        UserDefaults.standard.synchronize()
    }
    
    override func tearDown() {
        super.tearDown()
        UserDefaults.standard.removeObject(forKey: LoginStateUDConst.isLogined)
    }
    
    func testConfig() {
        
        //UDのキー名は正しいか？
        XCTAssertEqual(LoginStateUDConst.isLogined, "isLogined")
    }
    
    func testInit() {
        
        //初期表示は正しいか？
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログイン")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.blue)
    }
    
    func testLogin() {
        
        //ログイン後の表示は正しいか？
        LoginUsecase().loginOrLogout()
        XCTAssertTrue(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログアウト")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.red)
    }
    
    func testLogout() {
        
        //ログアウト後の表示は正しいか？
        LoginUsecase().loginOrLogout()
        XCTAssertTrue(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        LoginUsecase().loginOrLogout()
        XCTAssertFalse(UserDefaults.standard.bool(forKey: LoginStateUDConst.isLogined))
        
        XCTAssertEqual(LoginUsecase().loginButtonTitle(), "ログイン")
        XCTAssertEqual(LoginUsecase().loginButtonTitleColor(), UIColor.blue)
    }
}
```