iOS大規模リファクタリング

こんにちは。Airシリーズ開発チームでiOSの開発リードを担当している永井です。

この度、Airレジから予約台帳機能を切り出して、Airレジレストランボードの2つのアプリとして新たに5/10にリリースしました。
iPad版・iPhone版合わせて181,175行あったAirレジですが、今回内部的にもObjective-CからSwiftに全面的に書き換えています。

まだまだリファクタリングしていきたい課題はありますが、コード行数は70%も減り(つまり元々の行数から30%になりました)、SonarQubeで示される技術的負債も500dから21dに減り、かなり成功したと言って良いのではないかと思っています。

今回の取り組みの中で、良かったこと・再検討したいことがいろいろ発見できました。それらについてまとめてみるので、これからSwift採用を検討している方々の参考になれば幸いです。

取り組みのポイント

振り返ってみて、やって良かったことや、参考にして頂けそうな内容について述べていきます。

Swift化採用のきっかけ

Objective-Cで開発していたプロジェクトでSwift採用に踏み切るのは簡単でない場合もあるかと思います。
我々も、検討開始当初はObjective-Cで進める想定だったのですが、開発着手前あたりでちょうどXcode 7 Betaが登場し、Swift 2が出たのでこれから安定していくだろうということで意外とすんなり話が通りました。

ただし、ノーリスクというわけにはいかなかったので、懸念された課題については以下のような前提の下でSwift採用が決まりました。

課題 前提
工数・リスクが予測しにくい 継続的なリファクタリング文化を形成することを本プロジェクトの目的におき、SwiftでObjective-C風の書き方を書くことは許容して進める。
学習コスト コーディング規約や設計指針を整備し、コードレビューなどで最大限サポートしていく。

上記前提のため、公開されている様々な規約を参考にさせて頂きながらコーディング規約を作成したりしました。
結局、SwiftはOSS化でかなり盛り上がりましたし、開発者のモチベーション向上にも貢献したので採用して良かったと思います。

品質基準

今回の取り組みは、アプリの分割・Swift化のみならず、生産性を高めるための品質向上も大きな目的の1つでした。Swift採用が決まってすぐに、SonarQube Swift PluginとXcode Serverを使った定期的な品質チェックの仕組みを導入し、以下のような品質基準を定めました。

SonarQube

項目 基準
Technical Debt Ratio 5%以下
Blocker 0
Critical 0

Xcode Server

項目 基準
Test Coverage 30%以上
Analyzer Warning 0
Warning 0

本当はもう少しテストカバレッジ上げていきたいところですが、元々がほぼゼロだったのでこれでもかなり改善したのではないかと思います。

クラス構成

旧Objective-Cプロジェクトはクラス構成に統一感がなく、どこに何があるかわかりにくかったり、ViewControllerにロジックが偏ってテストコードが書きにくいなどの問題が生じていました。
そこで今回は、以下のようにクラス構成の大枠を定めました。

1
2
3
4
5
6
7
8
9
10
11
Classes/
┣ AppDelegate.swift
┣ Protocols/
┣ Enumerations/
┣ Extensions/
┣ Managers/
┣ Entities/
┣ Models/
┣ Controllers/
┣ ViewModels/
┗ Views/
グループ 説明
Protocols プロトコルを配置する。プロトコル拡張の活用は推奨。
Enumerations 列挙型を配置する。その型に属する一般的な分岐処理にはEnumに処理させる(SwiftのEnumを活用する)。
Extensions 既存クラスの拡張を記述する。ファイル名は<対象+目的>.swift。
Managers ドメインごとのビジネスロジックを担当するクラスを配置する。
Entities ローカルデータベースに永続化する対象クラスを配置する。
Models Managers/Entitiesに属さないモデルクラスを配置する。
Controllers UIViewController派生クラスを配置する。
ViewModels ViewControllerの重い処理はViewModelとして分離する。ただし、RxSwiftなど大きく設計に影響するライブラリは利用しない(手動バインディング)。
Views UIView派生クラスを配置する。

Managersの責務が広範になってしまったり、Managers/Entities/Modelsの境界線が曖昧だったりするので、その辺りは今後検討・改善していく予定です。

スキーマ・ターゲット・コンフィギュレーション

旧プロジェクトでは、スキーマ・ターゲットが増え、ファイルのリンク漏れや設定値のずれの懸念が付いて回っていました。
スキーマは1つ、ターゲットはAirREGIAirREGITestsAirREGIUITestsに絞り、ビルド設定は開発用・AdHoc用・申請用の3つのビルドコンフィギュレーションで管理するようにしました。

依存管理

なるべくCarthageを利用するようにしました。

当初は使い方がうまく浸透せず不満が挙がることが多くありましたが、開発リードとして先手を打っていろいろ試して人柱になり、知見をためながらサポートに回ることでなんとか受け入れてもらうことができました。

Carthageが利用できないものについてはCocoaPodsを利用しています。

また、CocoaPodsの推奨に従い、Carthage/Checkouts/Pods/はGit管理下に置いています。
GitHub APIのアクセス(Carthage)やSpecsレポジトリへのアクセス(CocoaPods)も減るのでオススメです。

Web API通信

Alamofireを利用して、APIKitに近い構成になっています。

責務も分散され型安全にも書けるので、リクエスト・レスポンスの内容を構造体として管理するのはオススメです。

また、mitmproxyをチームメンバーに広めたりもしました。

文字列

画像やローカライズ文字列など、iOSでは文字列で指定することが多くありますが、放っておくとどうしても人的ミスが気になってきます。

開発当初R.swiftに出会い、感動しました。
触発されて、当時はまだR.swiftが対応していなかったローカライズ文字列のためのL.swiftを書きましたが、2.2.0からR.swiftの対応も完了したので、そちらに寄せていく予定です。

画像管理

Asset Catalogを利用し、それぞれはベクターで管理しています。
Airシリーズの他アプリにまだ3倍スケールに対応できていないものもありますが、そちらも対応するタイミングでベクターに移行していこうと考えています。

dkhamsing/ios-asset-namesを参考にして以下のように命名規則を定めています。

iPhone-ScreenName-ImageNameButton.pdf

  • 固まりごとハイフンつなぎ・固まり内はキャメルケース
  • iPhone/iPad:固有の場合は先頭にiPhone/iPad(共有の場合はUniversal
  • ScreenName:画面名(共通の場合はCommon
  • ImageNameButton:用途と要素名(ButtonとかIconとかで終わる)

命名規則は決めた方がR.swiftでサジェストを活用しやすくなって良いです。

プロトコルの活用

Swiftでは、Protocol Oriented Programmingというものが提唱されています。Web API通信用クラスにもプロトコル拡張を利用しています。

Airレジには注文・注文履歴という2つのエンティティがあり、どちらも似たようなプロパティを持っているので、OrderConvertibleというプロトコルを設けて共通処理にはプロトコル拡張を利用したりしています。

Enumの活用

強力なEnumも、Swiftの特徴の1つです。

典型的なところでは、エラー一覧をEnumで管理して、出力するエラーメッセージもEnumに持たせたりしていて、直感的でとてもわかりやすいです。

Airレジらしいところでは、伝票がアプリで新規作成されたものなのか、サーバーと同期済みなのか、削除されているのかというステータスがあり、それらを以下のように管理しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// レコードの状態を表すEnum
enum RecordStatus: String {
  /// サーバから取得後変更なし
  case Clean
  /// 新新規に作成され、まだクライアント側にしかない状態
  case Create
  /// サーバーから受け取ったものに変更を施した状態
  case Update
  /// サーバーから受け取ったものを削除した状態(同期完了後削除する)
  case Delete
  /// リクエストパラメータのkey
  var requestKey:String {
      switch self {
      case .Create:
          return "create"
      case .Update:
          return "update"
      case .Delete:
          return "delete"
      default:
          return ""
      }
  }
  /// レコードが変更された時に呼ぶ。現在のstatusがCleanの場合のみ、Updateに変わる。
  mutating func updateStatus() {
      switch self {
      case .Clean:
          self = .Update
      default:
          break
      }
  }
}

updateStatus()のようなロジックをEnumに寄せられるのはとても便利です。

概念のモデル化

ある概念を1つの構造体として包んで、そちらに処理を寄せると、責務が細分化できて良いです。

Airレジの良い例としてはPriceという構造体があります。Airレジはレジアプリなので複雑な金額計算があるのですが、Price構造体が、

1
private(set) var rawValue: Int64

をラップして、rawValueに対する金額計算処理をPrice構造体で管理するようにしています。

この設計自体はObjective-C時代でも可能でしたが、Swiftは四則演算・比較などのオペレータを構造体に対して定義できるおかげで、より活用しやすくなった印象があります。

今後検討したい課題

Storyboard廃止

今回Viewは主にStoryboard・XIBを利用して実装しています。個人的には「Storyboard使わない派」だったのですが、やっぱり日本はまだStoryboard派が大多数で。。

しかし最近try! Swiftで登壇者の多くがStoryboard使わない派だったことに勇気付けられたので、再検討してみたいと考えています。

Immutableなコードを書こうとすると、Storyboardはどうしても邪魔になってきてしまいます。SnapKitなどを使えばコードでAutoLayout書くのも楽ですし、フレームワークとして分離すればPlaygroundで結果を確認しながら書くこともできます。

iOSのマイクロサービス化

今回は、Airレジの注文会計機能と予約機能の分割を行いました。しかし、まだ集合体としては大きく、人間が全体を把握できる単位には分けられていません。

まだ明確に実行できていませんが、Dynamic Frameworkを活用すれば iOSアプリのマイクロサービス化 ができるのではないかと妄想しています。機能間のインタフェースを明確に定義して、機能の分離を完遂するのは少々骨が折れそうですが、今後検討していきたい課題です。

最後に

リクルートライフスタイルでは、Swiftを積極的に採用しています。Swiftでコードを書きたい方はぜひ弊社へ!