今回も引き続き、個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」を眺めていきます。そんな中から今日は
型拡張
まわりの小技的な話題を取り上げつつ、型拡張についての特色をあれこれ確認していけたらいいなと思っています。よろしくお願いしますね。———————————————————————————————————
熊谷さんのやさしい Swift 勉強会 #276
00:00 開始
00:34 @ViewLoading を深追いするのは別の機会に
01:49 今回はプロトコル拡張の話
03:16 プロトコル適合毎に拡張を分けるアイデア
04:22 型拡張による可読性の向上
07:40 複雑なプロトコルほど、この方法で可読性が向上する印象
08:34 プロトコルで分けることで保守性が高まる
09:44 型拡張を多用することについての観察
11:13 どのプロトコルに適合しているかの把握はしづらくなるかも?
17:10 型拡張では保存型プロパティーを追加できない
19:19 細かく分けて拡張する方が、コードを扱いやすくなる印象
20:55 データ構造と振る舞いとを分けたりもする
23:19 クラスはオーバーライドも気にして拡張での追加にしないことも
23:41 アクセスコントロール意識した型拡張
25:08 型拡張は広く応用できる
25:49 振る舞いを本体から分離したときの印象は?
26:55 型拡張での入れ子の型
28:35 クロージング
———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #276
はい、では始めていきましょう。今日は引き続き、田中亮香さんの提案に基づいて、Swiftコードの書き換えを検討します。新しい案件に進む前に、前回見た「バッドビューローティング」がどうやって実現されているのかをもう少し深く見ていきたいと思います。これを追求するのはデバッグには役立つと思いますが、かなりマイナーな内容ですので、いつか雑談の時間にでも試してみたいですね。
さて、今日の本題は「拡張エクステンションズ」です。これはプロトコルごとに準備のための実装を、その中で行うという主張がある提案です。この提案には、プロトコルの状況に必要な実装は、そのプロトコルへの変更のためのエクステンションとして作るべきという点が強調されています。これは、後からコードを読むときに非常に追いやすくなります。他にもメリットがあります。
まず、その「読みやすくなる」という点についてみんなで確認しましょう。エクステンションのお話を具体的なコード例を交えながら進めていきます。例えば、何かしらの型にプロトコルを当ててエクステンションで実装していくとします。
protocol CustomStringConvertible {
var description: String { get }
}
struct MyType {
var value: Int
}
extension MyType: CustomStringConvertible {
var description: String {
return "Value is \\(value)"
}
}
このようにエクステンションで
CustomStringConvertible
に準拠させると、例えば次のようにdescription
プロパティを実装できますね。MyType(value: 42).description // "Value is 42"
このエクステンションは、各プロトコルごとに実装をまとめることによって、コードが追いやすくなります。ただし、言語仕様的にできないこともあったりします。なので、全てのケースで読みやすいとは限らないということも念頭に置かなければなりません。
この提案のメリットについても、みんなで共有しながら確認していきたいと思います。他にどんなメリットがあるのか、実際にコードを書きながら検証してみましょう。
以上が本日の内容です。それでは、実際のコードを見ていきましょう。 要はカスタムストリングコンパリフレが要求しているプロパティについて話しています。これはMacOSで必要なものですが、具体的にどう実装するかというと、例えばプロパティをただずらずらと書いていくのではなく、必要に応じてプロトコルをエクステンションして実装するという提案です。
例えば、
Identifiable
というプロトコルを実装する場合に、単にすべてのプロパティを羅列するのではなく、必要なときにエクステンションで追加実装するのが良いということです。このように関連する機能を分けることで、何が要求されているのかが見やすくなります。この方法に賛成するかどうかについて「分けたほうが良さそうだ」と感じる人もいるでしょう。分けた方が将来的にプロトコルを外したいというときには、その場所をすぐに特定しやすくなります。ただ、プロトコルを外す機会は少ないかもしれません。
面白い質問として、「エクステンションはどれくらいの数が適切なのか?」があります。分けすぎると混乱するかもしれないし、分けなさすぎても読みにくくなります。例えばエクステンションが50個もあるとわかりやすさを保つのが難しくなります。
以下にエクステンションの具体例を示します。
extension YourType: Identifiable {
var id: String { return self.uniqueID }
}
このように、必要なプロトコル適合部分をエクステンションで追加することができます。また、他のプロトコルも同様にエクステンションで追加することが可能です。
extension YourType: Comparable {
static func < (lhs: YourType, rhs: YourType) -> Bool {
return lhs.value < rhs.value
}
}
このように実装を分けていくと、適用されたプロトコルや追加された機能が一目でわかります。多少分けすぎと感じるかもしれませんが、読みやすさとメンテナンス性を考えると、分けるメリットは大きいです。
この観点から言えば、エクステンションを使って関連する機能やプロトコル適合部分を分割して実装する方法は有益だと言えるでしょう。さらに、エクステンションを整理することで、新しい機能追加や既存機能の見直しを行う際にも役立つでしょう。 個人的に感じている弊害として、この場合は
Hashable
なのかなというところが見分けづらいことがあります。いい探し方があるのでしょうか? ただ、自分の場合は特にないんですよね。通常の開発であれば、上のアプリケーションバーにいろいろなツールがありますが、あまり使いこなせていない感じがします。例えば、コントロールキーでのコード補完機能を使えば、関連する部分が出てくるはずですが、それでも見つけるのに手間取ることがあるかもしれません。メイン関数やプロトコルも探さないと見つからないですね。実際にプロトコルが当てはまるかどうかは、メソッドを見て判断する必要があります。自動補完機能も多少のヒントを提供してくれますが、特定の場所に書かれていないと、最初の定義をたどる必要があります。
Hashable
やその他のプロトコルに関しては、一目瞭然でコードが見えるようになっていると使いやすいのですが、現状ではそうもいかないようです。プロパティの保存型やその他の特徴に基づいて、どちらに記述すべきか迷うこともあります。指示が分かりやすければ、どちらが適切かすぐに判断できるので、個別の要件に応じて決めています。今回の場合、保存型プロパティは拡張には持てないので、他の場所に定義するべきだと思います。実装の自動合成に関しても、どこに置くか悩ましいことがあります。コードをどちらに書くべきか迷ってしまいます。コードのフォールド機能が一発でできればもっとスマートに探せるはずですが、十分に機能が提供されていないことが多いです。エディタのナビゲーションやフォルディング機能もいまひとつと感じています。
今のところ、コメントブロックやメソッドだけを一発でフォールドできれば見つけやすいのですが、どうもその機能がないようです。Android StudioやXcodeなど高機能なエディタでも、この機能がより充実していると便利なのですが、現時点では不十分な感じがします。これからも探しながら使っていく必要がありますね。 自分で作ったものだったら何とか分かるんでいいですけど、どの変数がどれに定義されたかが分からなくなることがあります。プロパティの場合、エクステンションに置けないんですよね。そうなんです、離れていると不便なことがありますよね。
例えば、
RawRepresentable
なんかがその例です。このRawRepresentable
をエクステンションで導入しようとすると、value
に関してはRawRepresentable
を搭載できます。しかし、イニシャライザーが別の場所で搭載されていると、そのイニシャライザーも用意しなければならなくなり、それがコードの分離を引き起こします。このように、関連する部分が離れてしまうと、気になりますよね。この程度のRawRepresentable
ならいいですが、もう少し複雑なものになると問題です。プロトコルが成り立つ前提で最初の宣言で適合していない場合、やはり難しいですね。この折り合いがどちらがいいか感じるのは難しいですが、個人的にはこうやって分けるのが好ましいと思います。コードが明瞭になり、まとめるとすっきりします。
LosslessStringConvertible
に昇格しようとしたときも、イニシャライザーを追加するだけで対応できます。シーケンス操作に対応する場合なども含め、エクステンションを使って整理するのは良い方法だと思います。そのため、わざわざ
Deblobor
などに分けることはしないですね。Hashable
やDecodable
は別として、その辺りは多く気にせず確定しています。ダンプ関数のようなものに関しても、エクステンションで分けるタイプです。プロトコル準拠とは関係なく機能を切り出して、本体に必要なものは構造体として保存型プロパティを決め、初期化のための最低限のイニシャライザーも用意します。
Identifiable
のためのプロトコル準拠を気にしない場合、このように構築します。構造体の本体では、データ構造とそのデータ構造を初期化するためのイニシャライザー(デジグネイティブイニシャライザー)だけを置き、それ以外の機能はエクステンションで分けます。これは完全に好みですが、アクション的なものは本体に入れずにエクステンションにします。イニシャライザーも、コンビニエンスイニシャライザー的なものは外に分けて、コードの中でメンバー初期化をセルフに渡してもいいですね。こうやって実装を分けると、データ構造が素直に見えるので好みです。 なので、こうしています。クラスになっても基本的にはそうですね。オーバーライドがエクステンションでモジュールをまたいだりするとだめだったりしたと思います。クラスの場合は、アクションなども本体の中に載せることが多いですね。
そんな感じで分けていって、あとはこうやって搭載しているものもあります。例えば、このファイル内だけで使いたい特徴みたいなものはプライベートエクステンションを使って、アクセスコントロールレベルを明確に分けたりします。プライベートが多いですが、パブリックに向けて公開するインターフェイスも用意することがあります。このようにアクセスコントロールを明確にすることで、整理しやすくなります。
エクステンションは単なる整理だけでなく、エクステンション単位でアクセスコントロールやアイソレーションなどの調整もできるので、その辺りも面白いところです。これについては次回以降、時間の都合で詳しく見ていこうと思います。
エクステンションの基本的なところを押さえたので、本題に入る前の導入的なお話をしました。ここどうですかね?ボディの中に実装を追加するのも普通にやると思いますが、エクステンションで分けないでやることもあります。その辺の好みや経験によって違いますが、個人的には最低限のデータ構造をボディに入れ、その他の機能はエクステンションで分けると見やすい気がします。
コードが長くなる場合は、エクステンションで分けて外に出すことも考えます。例えば、
enum
を使っている場合など、エクステンションで分けることで整理しやすくなります。これは個人の好みや経験によるところが大きいので、自分に合った方法を試してみると良いかもしれません。また、先に定義が出てきて、後から使用するコードが登場する順番が逆になることもあります。これには少し気持ち悪さを感じつつも、必要に応じて分けて書いています。
エクステンションの雰囲気的なところを見てきましたが、時間になったので今日はこれぐらいにしましょう。また次回は、もっと細かいエクステンションの使い方を見ていきたいと思います。お疲れ様でした。


