MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

型を利用した状態管理について

iOS
March 13, 2023

はじめまして。

タクシーアプリ『GO』の iOS アプリを開発している高橋です。

プログラムが取り扱っているモデル・関数・フラグなどがどのような状態なのか・どのような値を期待しているのか・その画面で表示できるモデルはどのようなモデルなのかなど、プログラム内部で扱っている「状態」をいかにうまく・いかにシンプルに「管理」していくのかが、そのまま実装の速度やバグの発生頻度の改善につながっていくと考えています。

今回はそんな「状態管理」をプロパティではなく型を利用してできるようにしてみましたので、その紹介をしたいと思います。


1. 課題感のある実装

1.1 実装の概要

タクシーアプリ『GO』を利用することで、ユーザーは下記3種類のタクシーのうちいずれかを呼ぶことができます。

  1. 通常のタクシー
  2. 高級ワンボックス車『GO PREMIUM』(以下 プレミアム車両)
  3. アプリ専用車『GO Reserve』

そのためアプリ内部において、この3種類のタクシーのうちどのタクシーを呼んでいるのかを管理する必要があります。

このため、下記のような実装を行っていました。

class Order {
    var vehicleType: String = ""

    /* このフラグがtrueだと、通常のタクシー車両を呼んでいることを表します */
    var isNormalTaxi: Bool { 
        return vehicleType == "0"
    }

    /* このフラグがtrueだと、プレミアム車両を呼んでいることを表します */
    var isPremiumTaxi: Bool { 
        return vehicleType == "1"
    }

    /* このフラグがtrueだと、GO Reserveを呼んでいることを表します */
    var isReserveTaxi: Bool {
        return vehicleType == "2"
    }
}

この実装で呼び出し側が厳密に状態判定を行う場合、下記のようなコードを書く必要があります。

let order = Order()

if order.isNormalTaxi == true,
   order.isPremiumTaxi == false, 
   order.isReserveTaxi == false {
    print("通常のタクシーを配車中")
} else if order.isNormalTaxi == false, 
          order.isPremiumTaxi == true, 
          order.isReserveTaxi == false {
    print("プレミアム車両を配車中")    
} else if order.isNormalTaxi == false,
          order.isPremiumTaxi == false,
          order.isReserveTaxi == true {
    print("GO Reserveを配車中")    
}

もちろんこのコードを見て「いやEnumで実装しようよ」みたいに改善案が出てくるのは、とても適切なもののように思います。

ただ当初は状態Aだけを想定して設計していたが、機能追加により状態Bも状態Cも考慮しないといけなくなった、というシチュエーションは実は皆様あるあるなのではないでしょうか。

このコードも当初通常車両だけだった配車状態が、機能強化によりプレミアム・GO Reserveと配車できる車両が増え、判定ロジックが意図せず煩雑あるいは冗長になってしまう、という意外と「あるある」な経緯をたどったコードだと考えています。

1.2 具体的な課題感

状態管理を改善できないかという視点で前述のコードを読んでみると、パッと思いつく範囲でも下記のような課題感があります。

  1. 各状態は仕様上排他関係であるにも関わらず、コード上はその表現ができない
  2. 機能拡張をすればするほどパターンが増え続けていく

それぞれについてより詳しく説明します。

1.2.1 各状態は仕様上排他関係であるにも関わらず、コード上はその表現ができない

Orderクラスにある3つのBoolの組み合わせは8パターン存在するため、Bool値の組合せと期待する処理を対応させた表を作ってみると下記のようになります。

An image from Notion

前述したようにタクシーのタイプは「3パターンのうちのどれか」です。

Boolの組み合わせは8パターン存在する一方で、仕様上意図した状態はわずか3パターンしかなく、仕様上存在しない状態が5パターンになり、全体の62.5%を占める状態は、仕様上存在しない状態になります。

そのためプログラムを実装するときは仕様上意図しない状態が表現されうることを常に頭の片隅に入れて注意深く実装する必要があることがわかります。

1.2.2 機能拡張をすればするほどパターンが増え続けていく

前述したように、現状でも表現できる状態のうち62.5%は仕様上存在しない状態でした。

更にもう一つのタクシーが呼べるようにこのままのやり方で機能拡張を行った場合、組み合わせが16パターンに対し意図している状態は4パターンになり、表現できる状態のうち75%は仕様上は存在しない状態、つまりプログラムとして処理してはいけない状態になります。

更にもう1つのタクシーを増やすと、組み合わせが32パターンに対し意図している状態はわずか5パターンになり、表現できる状態のうち84.375%が仕様上存在しない状態になります。

これは新しく呼べるタクシーが増えるたびに単純に数というベクトルから見ても状態の管理が難しくなることに加え、下記のようなイメージで「既存実装箇所についても適宜改修を入れないと意図せぬ動作が発生する可能性がある」ことになります。

let order = Order()

if order.isNormalTaxi == true,
   order.isPremiumTaxi == false, 
   order.isReserveTaxi == false,
   order.is新規のタクシー == false {New!
    print("通常のタクシーを配車中")
} else if order.isNormalTaxi == false, 
          order.isPremiumTaxi == true, 
          order.isReserveTaxi == false,
          order.is新規のタクシー == false {New!
    print("プレミアム車両を配車中")
} else if order.isNormalTaxi == false,
          order.isPremiumTaxi == false,
          order.isReserveTaxi == true,
          order.is新規のタクシー == false {New!
    print("GO Reserveを配車中")
} else if order.isNormalTaxi == false,
          order.isPremiumTaxi == false,
          order.isReserveTaxi == false,
          order.is新規のタクシー == true { 
    print("新規のタクシーを配車中")
}

上記の変更を行う場合、いくら軽微な修正とは言えコードは変更されているので動作チェックを行う必要があったり、既存の条件式の修正を忘れてしまうと別のバグが発生してしまったりと、メンテナンスの観点でも課題があると言えます。

今回は上記2つを改善できる点として捉えましたが、2つの点から見るだけでも大きく改善できる余地がある事がわかりました。

2. 改善案について

この実装を改善するため、今回はProtocolとStructを用いた下記のような実装を行いました。

protocol OrderTaxiType {
    init?(vehicleType: String)
}

class Order {
    var vehicleType: String = "" 
    
    var taxiType: OrderTaxiType {
        if let reserveTaxi = Order.ReserveTaxi(vehicleType: vehicleType) { return reserveTaxi }
        if let premiumTaxi = Order.PremiumTaxi(vehicleType: vehicleType) { return premiumTaxi }
        if let normalTaxi = Order.NormalTaxi(vehicleType: vehicleType) { return normalTaxi }
        
        return Order.NormalTaxi()
    }
}

extension Order {
    struct NormalTaxi: OrderTaxiType {
        init() { }
        init?(vehicleType: String) {
            guard vehicleType == "0" else { return nil }
        }
    }
    
    struct PremiumTaxi: OrderTaxiType {
        init?(vehicleType: String) {
            guard vehicleType == "1" else { return nil }
        }        
    }
    
    struct ReserveTaxi: OrderTaxiType {
        init?(vehicleType: String) {
            guard vehicleType == "2" else { return nil }
        }
    }
}

このコードでは下記のようなクラス図となっています。

An image from Notion

上記実装において、表現できる状態は3パターンのうちプログラムとして意図したパターンは3パターンとなり、仕様上存在しない状態が表現できる問題はなくなりました。

それだけではありません。この変更により利用側では下記のように利用することが可能になり、フラグの==処理ではなくis演算子を利用してコードを記述できるようになります。

let order = Order()

if order.taxiType is Order.NormalTaxi {
    print("通常のタクシーを配車中")
} else if order.taxiType is Order.PremiumTaxi {
    print("プレミアム車両を配車中")    
} else if order.taxiType is Order.ReserveTaxi {
    print("GO Reserveを配車中")
}

これはつまり、新規にタクシータイプを追加したときでも下記のように「単純に新規の状態判定を追加するだけで良くなった」ということで、そのまま実装工数とテスト工数が削減され、結果として開発速度の向上につながっていきます。

let order = Order()

if order.taxiType is Order.NormalTaxi {
    print("通常のタクシーを配車中")
} else if order.taxiType is Order.PremiumTaxi {
    print("プレミアム車両を配車中")    
} else if order.taxiType is Order.ReserveTaxi {
    print("GO Reserveを配車中")
} else if order.taxiType is Order.新しいタクシー {
    print("新規のタクシーを配車中")
}

3. 代替実装について

実はこの改善をSwiftで行う際は下記のような形でProtocolとStructの代わりにEnumを用いることも可能です。

class Order {
    var vehicleType: String = "0"

    var taxiType: Order.TaxiType {
        return Order.TaxiType(vehicleType: vehicleType) ?? Order.TaxiType.normalTaxi
    }
}

extension Order {
    enum TaxiType {
        case normalTaxi, premiumTaxi, reserveTaxi
        
        init?(vehicleType: String) {
            if vehicleType == "0" { self = .normalTaxi }
            if vehicleType == "1" { self = .premiumTaxi }
            if vehicleType == "2" { self = .reserveTaxi }
            
            return nil
        }
    }
}

let order = Order()

print(order.taxiType) // normalTaxiが出力される

4. おわりに

今回は、Boolによるフラグ管理ではなく、型情報を用いた状態管理を導入することによってコードの実装をいかにかんたんにすることができたかを、実際の事例を交えながら紹介させていただきました。

現時点の『GO』においては3種類なのでまだ管理ができなくもないですが、将来的に機能拡張をしていく上で技術的なネックになると感じていたので、改善することができてよかったです。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @goinc_techtalk のフォローもよろしくお願いします!