ユニットテストを実践して感じた、初心者がiOSアプリ開発で陥りやすい 3つの悩みとその分析

# はじめに
こんにちは、iOSエンジニアの [dayossi](https://twitter.com/dayossi) です。

家族が幸せになれるサービスを提供したいと思って、
[HaloHalo](https://hallo-hallo.sakura.ne.jp/home/)という家族日記アプリをリリースしています。

今回は、ユニットテストを通じてアプリ開発をして感じた
3つの悩みについて分析しました。

今回の成果物はコチラです。
[https://github.com/Kohei312/TDD_Practice_Poker](https://github.com/Kohei312/TDD_Practice_Poker)


# これまでよく陥っていた悩み
テストを書かずに開発していた時は、
以下の3つの悩みを切り分けられず、よく悩まされていました。

・UIKitを中心とした、View構築のロジックがおかしいのか...?
・UIを表示するためのデータがおかしいのか...?
・データの受け渡しがおかしいのか...?

ユニットテストを書くことで
このあたりの切り分けがスムーズに行うことができ、
問題を分析しやすくなりました。

問題を分析する際に、以下の3つの視点が
共通して有効だったと感じました。


## 1. ちゃんと責務を分離できているか？

オブジェクト指向の原則であるSOLID原則の1つ ```単一責務の原則``` と、
設計方針・意図を意識できているかを確認する問いかけです。

今回は「レイヤー、処理ごとに目的を明確にする」というニュアンスで使用しました。

## 2. 状態の変化を可視化できているか？
　
1つ目のポイントと重複する部分がありますが、
どの処理を、どのタイミングで呼び、どんな変化が起こっているのか明確にすることです。

特に引数が多くなって、複数の処理を同時に行おうとすると混乱しやすく
それゆえコードミスも起こりやすいと感じました。

enumで状態を列挙しておいたり、特定のレイヤーでのみプロパティの変更を行う処理を定義しておくと、
状態変化を把握しやすかったです。

## 3. 適度にレイヤーを分割できているか？

ここも1つ目のポイントと重複しますが、
全体設計どおりにレイヤーが機能しているかを逐次確認しました。

各レイヤーがなんの状態を管理するのか、目的を確認しながら
徐々に責務を分離していくと、見直しも行いやすかったです。


以下、今回のアプリ開発で実際に取り組んだ事例を取り上げていきます。

# 事例：状態が共有されない
今回作成したポーカーゲームのなかで、
プレイヤーは3回までカードを交換できるルールを設けました。
(プレイヤーは、ユーザーとCPUの2人という構成です)


その中で、各プレイヤーがカードを交換した回数に応じて
ゲームの状態が変化する、という点でつまづきが起きました。


交換回数を changeCount という変数で
Playerというプレーヤーの状態を持つ構造体のプロパティとして保持し、
ルールに沿って制御するようにしていました。

PlayerTypeというenumにて、プレーヤーがユーザーかコンピュータなのか
判別するようにしています。

~~~swift:Player.swift
public enum PlayerType{
    case me
    case other
}
struct Player{
    
    var playerType:PlayerType
    
    init(playerType:PlayerType){
        self.playerType = playerType
    }
    
    var playerStatement:PlayerStatement = .thinking
    var changeCount = 3
}
~~~


最初はゲーム全体のロジックを管理する PokerInteractor という上位レイヤーに
直接Player型のインスタンスを置いて管理していました。

~~~swift:PokerInteractor.swift

// MARK:- 各プレイヤーのカード交換回数、状態を管理
public struct PokerInteractor{

    var player_me:Player
    var player_other:Player
    
    mutating func changePlayerCount(_ playerType:PlayerType){ 
       switch playerType{
         case .me:
              player_me.changeCount -= 1
         case .other:
              player_other.changeCount -= 1
       }
    }
~~~

ビジネスロジックを取りまとめるレイヤーとして位置付けており、
ここでプレイヤーの状態と、ゲームの進行状態をコントロールしていました。

ですが、ここに落とし穴がありました。

片方のプレイヤーのターンでは、そのプレイヤーのカード交換回数はちゃんとカウントされていましたが
もう一方のプレイヤーのカード交換回数が共有されないのです。

player_meのターンで、player_meのchangeCountは確かに減っているのに
player_otherのターンになると、player_meのchangeCountが初期値に戻っているのです.

~~~swift:PokerInteractor.swift
public struct PokerInteractor{

#WARNING("いつまでも、状態が共有されない...")
    var player_me:Player
    var player_other:Player
    
    mutating func changePlayerCount(_ playerType:PlayerType){ 
       switch playerType{
         case .me:
              player_me.changeCount -= 1
         case .other:
              player_other.changeCount -= 1
       }
    }
~~~

## 問題点：ロジックデータの管理に問題がある

テストでは、実際に計算できていることは確認できており
View側での構築エラーもみられなかったので、ロジックデータの管理に問題があると考えました。

## 分析：メモリポインタの変更を考慮できていなかった 
changeCountはイミュータブルな値なので
値を変更する際は、生成したPlayer型のインスタンスから変更を指示する必要があります。

ただ、値型であるPlayerのプロパティを更新すると、Player全体の値が更新され
Playerをプロパティにもつ上位レイヤーのPokerInteractorも更新されます。

そのため、PokerInteractorで管理していた 2つのPlayerは
結果的に新しいインスタンスが再生成されることとなり、

カードの交換カウントが毎回初期値へリセットされてしまって
プレイヤー全員の状態を共有することができなくなっていました。

そこで、Player全員の状態を把握するための参照型PlayerStatusを一つ追加し
Playerの状態を共有できるように変更して対応しました。

## 対策：PlayerとPokerInteractorの間に、参照型レイヤーを追加した

PlayerStatusを参照するメモリ領域は常に同一であるため
各プレイヤーの値が更新され、メモリポインタが変更されても
常に変更後の値をスコープできることを狙いとしました。


~~~swift:PlayerStatus.swift

final class PlayerStatus{    

    var players:[Player] = []
    var interactorInputProtocol:InteractorInputProtocol?
    
    subscript(playerType:PlayerType)->Player{
        get{
            return players.filter({$0.playerType == playerType}).last!
        }
        set(newValue){
            if let player = players.filter({$0.playerType == playerType}).last{
                for (index,p) in players.enumerated() {
                    if p.playerType == player.playerType{
                        players.remove(at: index)
                        players.insert(newValue, at: index)
                    }
                }
            }
        }
    }

    func decrementChangeCount(_ playerType:PlayerType){
        
        self[playerType].changeCount -= 1
        interactorInputProtocol?.checkGameStatement(playerType)
        
    }
}
~~~
配列内にPlayerクラスを格納し、subscriptで必要なプロパティを抽出できるようにしましたが
計算コストが高い上にネストが深く読みづらいので、

今回のアプリのように登場人物が限られるケースでは、
それぞれインスタンスを分けて保持したほうが
わかりやすくて良かったかなと思います。

注意点として、どこからでもPlayerStatusのプロパティを変更し
その状態を共有できてしまうため
計算処理も、PlayerStatusから行うように統一しました。


## 考察：各モジュールの責務分離があいまいだった

以上で、各プレーヤーの状態管理をPlayerStatusが担うことも明確にし
PokerInteractorからは、状態変更を指示するだけにしました。

言い方を変えれば、ビジネスロジックを管理するPokerInteractorでの
責務は分散できる余地があったことを見逃していたといえます。

テストを通してビジネスロジックの一つ一つの処理は
ちゃんと動いていることを確認できていたため、

PokerInteractorレイヤーの責務が複雑になっていることに
気づくことができたと思います。


# まとめ

自分が体験した中で、よく陥りやすい3パターンを抽出しましたが
ホントに基本的なことばかりでお恥ずかしい限りです。

原則を外れてしんどかった部分が多くを占めていることを、改めて実感できました。
もっと上手に設計原則を生かせるように精進します。

温かいツッコミを、お待ちしております。

# 参考図書・記事
[TDD Boot Camp.TDDBC仙台07課題：ポーカー]
(http://devtesting.jp/tddbc/?TDDBC%E4%BB%99%E5%8F%B007%2F%E8%AA%B2%E9%A1%8C)

[松尾 和昭,細沼 祐介,田中 賢治 他.iOSテスト全書(2019).PEAKS出版.](https://peaks.cc/books/iOS_testing)

[関 義隆,史 翔新,田中 賢治 他.iOSアプリ設計パターン入門(2019).PEAKS出版.](https://peaks.cc/books/iOS_architecture)

[田中 賢治. Swiftで書いておぼえるTDD(2018).株式会社インプレス R&D.]
(https://nextpublishing.jp/book/10137.html)


[キーワード]
TDD駆動設計：
[Dan Chaput, Lee Lambert, Rich Southwell. What is an Enterprise Business Rule Repository?. MODERA analyst.com.]
(http://media.modernanalyst.com/New_Wisdom_Software_Webinar_-_PRINT.pdf)
Value Semantics：
[Yuta Koshizawa. Value Semantics とは. Heart of Swift](https://heart-of-swift.github.io/value-semantics/)
[Yuta Koshizawa. Value Semantics を持たない型の問題と対処法. Heart of Swift](https://heart-of-swift.github.io/value-semantics/problems-without-value-semantics)
[Yuta Koshizawa. Swift が値型中心の言語になれた理由とその使い方. Heart of Swift](https://heart-of-swift.github.io/value-semantics/how-to-use-value-types)
Copy-on-Write:
(SwiftにおいてCopy-on-Writeは問題にならないと思う)[https://qiita.com/koher/items/8c22e010ad484d2cd321]
(Swift での Copy on Write の実装方法の解説)
[https://qiita.com/omochimetaru/items/f32d81eaa4e9750293cd]
(swiftで 依存関係逆転の原則 を使ってテストしやすい設計にする)[https://qiita.com/peka2/items/4562456b11163b82feee]

VIPER:
(VIPERアーキテクチャでプロダクトのiOSアプリを1から作ったまとめ)[https://qiita.com/hirothings/items/8ce3ca69efca03bbef88]

SOLID分析：
(SwiftでわかるSOLID原則 iOSDC 2020)[https://speakerdeck.com/k_koheyi/swifttewakarusolidyuan-ze-iosdc-2020]
(iOS開発の事例に寄せたSOLID原則の解説)[https://zenn.dev/k_koheyi/articles/019b6a87bc3ad15895fb]

メモリ:
(Swiftのメモリレイアウトを調べる)
[https://qiita.com/omochimetaru/items/64b073c5d6bcf1bbbf99]
(素晴らしいSwiftのポインタ型の解説)
[https://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052]
(Memory Safety)
[https://docs.swift.org/swift-book/LanguageGuide/MemorySafety.html#//apple_ref/doc/uid/TP40014097-CH46-ID571]

値型:
[ミュータブルな型とイミュータブルな型の相違を知ろう](https://qiita.com/makotoo2/items/fc3a617882916f9775f5)
[純粋値型Swift]
(https://qiita.com/koher/items/0745415a8b9842563ea7)

subscript:
[Swift の Subscript について](https://qiita.com/happy_ryo/items/72b68859ed8ace9f5fb4)

protocol指向：
[WWDC 2015 Swiftで値型でより良いアプリを作る](https://qiita.com/eytyet/items/75e6c839d675c442d493)

enum:
[Swiftの列挙型(enum)おさらい](https://qiita.com/hachinobu/items/392c96820588d1c03b0c)
[[Swift] enumはprotocolに準拠できるので、例えばComparableによってシンプルに比較できる](https://qiita.com/y-some/items/ddc24781fbf3e1e96738)