こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 31 回です。Weekly Report については、第 1 回の記事を参照してください。
同じ釜のプロパティ
「緯度/経度」や「場所の ID」で位置情報を登録するサービスを実装しているとしましょう。この位置情報のデータモデルは GeoLocationPinModel
というクラスで表現するとします。このとき、直和型を使うことで「位置情報は必ず緯度/経度か場所 ID のどちらか一方で示される」ことを型検査で保証できます。Kotlin や Java では、直和型は sealed class
や sealed interface
で実現できます。(sealed
の子クラスのプロパティを取得するためには、ダウンキャストが必要になりますが、このダウンキャストは問題ないことが多いです。一方で sealed
以外に対するダウンキャストは、できる限り避けたほうがよいです。)
以下の GeoLocationPinModel
は sealed interface
として実装されていますが、プロパティの構造が適切であるとは言えません。
このデータモデルでは、LatLon
と Place
で共通するプロパティ (例: userId
) を取得するためにもダウンキャストが必要です。そのため、共通のプロパティを取得するコードが煩雑になってしまいます。
そこで以下ように、共通のプロパティを親クラスに抽出することで、ダウンキャストなしで取得できるよう改善を試みました。
しかし、この実装にはまだ改善の余地があります。それは何でしょうか?
共通のプロパティを抽出する
改善後のコードでは、共通のプロパティを追加・変更・削除しようとした場合に、GeoLocationPinModel
、LatLon
、Place
のすべてのクラスを更新しなけければなりません。また、LatLon
や Place
の定義が煩雑で、「何のプロパティを持つか」が一目ではわかりにくい点も問題です。
このようなときは、 共通のプロパティを直和型の外に抽出する ことで、より分かりやすい構造になる可能性があります。以下の実装では、sealed interface
には共通のプロパティを持たせず、sealed
でないクラスに共通のプロパティを持たせています。userId
などの共通のプロパティを使いたいときでも、GeoLocationPinModel
の定義を見れば十分に理解できるようになります。
直和型から共通のプロパティを分離することには、以下のような利点があります。
- どれが共通のプロパティで、どれが型固有のプロパティであるかが明確になる
- 共通のプロパティを追加・変更・削除する際のコード変更量が減る
- (
sealed interface
の場合) 共通のプロパティを親に持たせないことで、computed property でないことを保証できる
もちろん、プロパティの抽出が不要なケースもあります。型固有のプロパティが重要で、共通のプロパティは付随的に過ぎない場合などが当てはまるでしょう。抽出する・しないコードを見比べ、どちらがより分かりやすく取り扱いやすいかを検討してください。
構造的部分型付けの場合
TypeScript のような構造的部分型付けの言語では、以下のように、共通のプロパティを直接取得することができます。そのため、共通のプロパティを抽出する利点は、比較的小さくなります。
しかし依然として、共通のプロパティを抽出することには、以下の 2 つの利点があります。
- どれが共通のプロパティで、どれが型固有のプロパティであるかが明確になる
- 共通のプロパティを追加・変更・削除する際のコード変更量が減る
構造的部分型付けの言語でも、共通のプロパティを抽出するか否かでどのようなメリット・デメリットがあるのか、コードを比較することが重要です。
一言まとめ
直和型に共通するプロパティを抽出することで、より分かりやすい構造にできることがある。
キーワード: sum type
, sealed class
, common property