こんにちは。iOSアプリエンジニアのKiichiです。
先日開催されたtry! Swift Tokyo 2024のLINEヤフー企業ブースでは、Code Review Challengeを行いました。Code Review Challengeとは、Bad CodeをGood Codeにするための公開コードレビューです。参加者に技術への関心を持ってもらうこと、外部の方々からのレビューにより社員の学びにつなげることを目的に、過去のイベントで行っていた取り組みを今回のイベントでも実施しました。
本記事では、今回出題したCode Review Challengeの1問目の解説をします。
出題コード
これは、TODOリストアプリのコードです。
サーバー上にあるTODOの一覧表示と、TODOの追加機能が実装されています。
このコードには複数の問題点がありますが、その中でも特に重要かつやってしまいがちな以下2点について解説します。
- 不十分なTaskの扱い
- 可読性・保守性の悪いenum設計
1. 不十分なTaskの扱い
このコードでは、サーバーとの通信を行う際にTaskを用いています。Taskは簡潔に非同期処理を書けるため便利ですが、キャンセル処理やエラーハンドリング・スレッドの扱いには注意が必要です。
まずは以下のTODO一覧取得箇所を見てみましょう。
画面が表示されたタイミングで取得処理を行っていますが、処理中に画面が非表示になっても処理は継続されます。そのまま画面が再表示されると、処理結果が重複してしまう可能性があります。onAppearの代わりにtaskを用いるか、onAppearが必要な場合は同時にonDisappearでキャンセル処理をするのがベターです。
続いて、新たなTODOを追加する処理です。
Task.detached は処理をメインスレッドから逃すために使われることがありますが、今回のケースではdetachedをつける必要はありません。というのも、@MainActorでないasync関数は、detachedかどうかによらずバックグラウンドで実行されるからです。さらに、後続のUI更新処理であるshowToastはメインスレッド上で動作すべき関数なので、なおさらdetachedは不要です。
また、エラーハンドリングが行われておらず、ユーザー視点では処理中なのか失敗したのか判断できません。
そして、別の部分にキャンセル処理が記述されているにもかかわらず、ここではキャンセルされた場合の挙動が定義されていません。
TODOService.shared.addの実装によってはshowToastまで実行され、キャンセルしているのに “Added successfully” と表示されてしまいます。awaitした後は Task.isCancelledをチェックし、キャンセルされている場合は処理を中断するようにしましょう。
さらに、今のままでは処理が終了したことが別の箇所から判別できません。Taskの最後に “addingTask = nil” の1行を挿入し、終了を表現すると良いでしょう。
2. 可読性・保守性の悪いenum設計
まずは以下をご覧ください。
TODO.statusが取り得る値は.completed、.inProgress、nilの3種類あります。ユーザー視点でTODOが進行中のときはstatus: .inProgress、完了済みのときはstatus: .completedだと分かります。では、nilは何を表しているのでしょうか? まだTODOを開始していない状態なのかもしれませんし、途中でキャンセルした状態を表している可能性もあり、確かなことは言えません。このような状況を回避するためには、”意味のある nil” を使用しないことが重要です。
のように、statusをnon-optionalにしつつ具体的なcaseを追加すると、status が何を表しているか分かりやすくなります。
続いて、このenumを使用している部分も見てみましょう。
今はdefaultに当たるのはnilの場合だけですが、将来enumのcaseが追加される可能性もあります。その都度使用部分でもcaseを追加すれば良いのですが、そのままでもコンパイルエラーにならないため、見落としが発生しやすいです。defaultの代わりに具体的なcaseを記述するようにしましょう。
その他の改善点
その他、以下のような改善点が考えられます。
コーディングスタイル
- 外部から使用しない変数や関数にはprivate修飾子をつけます。特に@Stateプロパティは外部から隠した方が良いです。
- protocolを型として用いる(存在型の)場合、anyキーワードをつけます。(例:Task<void, error=""> → Task<void, any="" error="">)</void,></void,>
- optional型の宣言に “= nil” は不要です。
- foregroundColor(:)は将来的に非推奨になるため、foregroundStyle(:)を用います。
- if letの省略記法を用いて簡潔に記述できます。(例:if let addingTask = addingTask {} → if let addingTask {})
アーキテクチャと設計
-
TODOStatusはTODOの状態を表す用途しかないため、以下のようにTODO名前空間を活用すると分かりやすくなります。
-
シングルトンを使用せず、TODOServiceを依存関係として注入できるようにすることで、テスト性向上を図れます。
- Image(systemName:)は再利用する可能性が高いため、以下のようにまとめておくと良いです。ライブラリや自動生成も役に立つでしょう。
- 実装にもよりますが、現状ではTODOServiceはclassである必要がないため、structにすると良いです。classが必要な場合はfinal修飾子をつけるべきです。
UX向上/バグ回避
- @StateプロパティのdidSetは、Binding型と併用した場合などにうまく機能しないことがあるため、onChangeを用いた方が良いです。(ただしEquatableへの準拠が必要)
- TODOの中身が同じ場合にListのidが重複してしまうので、TODOをIdentifiableに準拠させ、UUIDのような一意なidを用いるべきです。
- addingTaskの最後にTODOService.shared.fetch()を行わないと、TODO追加時にリストが更新されません。
想定解答
上記の問題点をすべて解決した想定解答は以下です。
まとめ
この記事では、Code Review Challengeの1問目の解説として、主に以下の点について説明しました。
- Taskを使って非同期処理する際のキャンセル処理・エラーハンドリング・スレッドの扱いに注意して行う必要がある
- 特にenumを使う際には “意味のある nil” を避け、switch文ではdefaultの代わりに具体的なcaseを記述した方が良い
入社1年目で新たにチームに参加したエンジニアとして、チーム開発における可読性や保守性の重要性を強く感じています。コードレビューによって健全なコードを後世に残していけたらと思います。
また、今回はコードを見て指摘する問題でしたが、ユニットテストをしっかり書けば気づけるような点、linterやformatterが自動で指摘してくれるような点も多くありました。それらも活用し、効率的にコードレビューできるようになると良いと思っています。