Studyplus iOSにおけるコードリファクタリングへの取り組み
こんにちは、Studyplus事業部モバイルクライアントグループの上原です。以前はWidgetの実装についてブログを書きましたが、今回はStudyplus iOSで取り組んでいるコードのリファクタリングについて書きます。
モバイルクライアントグループでは、コードのリファクタリングを積極的に進めており、直近では下記を並行して作業しています。
- Objective-CからSwiftへの移行
- Codable対応
- 通信処理の改善
- 画面遷移処理のCoordinatorパターン準拠対応
Objective-CからSwiftへの移行
Manual Layoutで書かれた部分をAuto Layoutに修正
Objective-Cで書かれたコードは、作られた当時のままManual Layoutでframe計算をしていました。 Swiftへの移行の際に、Viewも新たに定義しAuto Layoutでのレイアウトの変更をしました。 これをしたことにより、今後のViewの変更がやりやすくなったのと不要な高さの計算メソッドなどが無くなりコードの可読性が向上しました。
テストの追加
移行の際に既存コードと同じ動作をしているかどうかを検証するためにテストコードを追加しました。 テストコードを書くことによって、元の動作が何をしているかを把握しやすくなります。 同時に、そのメソッドが実際に動作しているかを確かめることができるようになります。 変更の際に壊れたかどうかが分かるようになり、開発効率が良くなりました。
最近、上記が完了し、Swift100%でコードが書けるようになり開発がよりしやすくなりました。
Codable対応
既存レスポンスとリクエストで辞書型をパースしたり辞書型を渡していたものをクラスや構造体に変更
参考として、レスポンス解析の新旧コードについてサンプルを記載します。
final class SampleResponse { private(set) var sampleString: String? init(attributes: [AnyHashable: Any]) { sampleString = attributes["sample_string"] as? String } }
上記のようにinitializeに辞書型を渡して辞書型からパースしていた処理を、Decodableを利用して簡潔に書くことができるようになりました。
final class SampleResponse: Decodable { private let sampleString: String? }
上記の例では、sampleStringでキャメルケースになっていますが、APIから受け取っているレスポンスのキー名はスネークケースになっています。 デコード時に以下のように指定することで、キャメルケースのプロパティ名でもCordingKeysを都度定義しなくて済んでいます。
let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase
通信処理の改善
API呼び出し部分での成功や失敗の際のクロージャをResult型を渡すクロージャに修正
func postSampleAPI(sampleId: Int, success: @escaping (SampleResponse) -> Void, failure: @escaping (SampleError) -> Void, finally: @escaping () -> Void)
success, failure, finallyで3つ用意されていたクロージャをcompletionでResult型を返すクロージャに変更しました。 この変更によって内部が複雑化した時にクロージャの呼び忘れを減らせたのと、呼び出す必要のなかったクロージャを呼び出さなくて済むようになりコードの見通しが良くなりました。
func postSampleAPI(sampleId: Int, completion: @escaping (Result<SampleResponse, SampleError>) -> Void)
画面遷移処理のCoordinatorパターン準拠対応
前回のブログで触れられていたCoordinator Patternの導入について紹介します。
画面間の遷移の処理をCoordinatorに任せ画面遷移のロジックをCoordinatorだけで管理する
対応していて感じたメリットは以下のとおりです。
- 肥大しがちなViewControllerから画面遷移のロジックを移すことができ、ViewControllerの肥大化が抑えられる
- 本来渡す必要のなかったオブジェクトから必要なデータのみを渡す実装にしていける
実装について簡単に説明します。
Coordinatorプロトコルを定義し各Coordinatorクラスで準拠させます。
下記の例では、 SampleCoordinator
が SampleViewController
の画面遷移の責務を持っています。
画面遷移を行う場合は SampleCoordinator
の start
を呼び出し、 SampleViewController
を生成し表示させます。
SampleViewController
で画面遷移を行う時には delegate
の transitionSampleDetail
を呼びます。
この呼び出しにより、 SampleCoordinator
で遷移処理が行われます。
この実装によって SampleViewController
は画面遷移の責務を持たなくなります。
コードの肥大化が抑えられ、画面遷移は Coordinator
を見れば分かるといった点で、分かりやすくできました。
protocol Coordinator { func start() } final class SampleCoordinator: Coordinator { private let navigator: UINavigationController private let sampleViewController: SampleViewController? init(navigator: UINavigationController) { self.navigator = navigator } func start() { let sampleViewController = SampleViewController(delegate: self) navigator.pushViewController(sampleViewController, animated: true) self.sampleViewController = sampleViewController } } extension SampleCoordinator: SampleViewControllerCoordinatorDelegate { func transitionSampleDetail() { let viewController = SampleDetailViewController() navigator.pushViewController(viewController, animated: true) } } protocol SampleViewControllerCoordinatorDelegate: AnyObject { func transitionSampleDetail() } final class SampleViewController { private weak var delegate: SampleViewControllerCoordinatorDelegate? init(delegate: SampleViewControllerCoordinatorDelegate) { self.delegate = delegate } private func transitionDetail() { delegate?.transitionSampleDetail() } }
リファクタリングの進め方
上記の各リファクタリングは規模が大きく、変更する点が多岐に渡ります。 このリファクタリングを進めていくにあたってどのように作業しているかと言うと、例えばCoordinator準拠対応については以下の通りです。
- GitHubのProjectsへリファクタリングのテーマでボードを作成
- 対象は画面のため、アプリプロジェクト内のファイル名末尾
ViewController.swift
で検索 - 検索結果のクラスを1件ずつ、ToDoのカラムへチケットとして起票
- 該当クラスについて作業する際に、作業中のカラムへチケットを移動
- 作業が終わったらPRを作成しコードレビューに出し、コードレビューの欄にチケットを移動
上記の作業をするようにしたおかげで、作業が被らず、進捗がすぐに確認できるようになりました。 また、たくさんあったチケットが減っていくのも達成感があり良かったです。
さいごに
モバイルクライアントグループで取り組んでいるリファクタリングについて書かせていただきました。 このリファクタリングを進めていくことで、コードの可読性やコードの削減などが見込め開発効率が上がっていくことが想定されるので頑張っていきます!