サブクラス化
※以下はobjc.io, Issue #13 Architecture, Subclassingの日本語訳です。
著者:Chris Eidhof
この記事は、私がいつも書いている記事と少し異なります。
ガイドと言うよりも思考とパターン集です。私が述べるほとんどすべてのパターンが堪え難い方法で見つかっている。:すなわちミスを犯すことによって。決して、私は自分自身がサブクラス化することの権威者であると思いません。しかし、私は学んだいくつかのことを切に共有したいのです。この記事を決定的なガイドとして読まず、むしろ用例集として読んでください。
OOP(オブジェクト指向プログラミング)について尋ねられるとき、アラン・ケイ(発明者)は、それがクラスについてでなく、むしろメッセージングについてであると、書きました。それでも、多くの人は、クラス階層を作成することに重点を置いています。本記事でサブクラスが便利ないくつかのケースを見て、複雑なクラス階層に対する代替手段に注意を払います。経験上、このことが単純で管理しやすいコードをもたらします。このトピックで書いてきた多くの事は、Clean CodeやCode Completeのような本で見つけることができ、どちらの本も推奨書籍です。
サブクラス化する時
まずは、サブクラスを作成することにより意味をなす、いくつかのケースについて説明しましょう。カスタムレイアウトでのUITableViewCell
を構築している場合は、サブクラスを作成します。一度レイアウトを始めると、これをサブクラスに移動することは意味をなす。同じことが、ほとんどすべてのviewに適用できる。うまく纏めたコードを持つだけでなく、再利用可能なオブジェクトを保持し、そしてプロジェクト間で共有できる。
コードをマルチプラットホームやマルチバージョンを対象とし、いくらかカスタムな部分を書く必要があると想定してください。そしてそれは、OBJIPhoneDevice
とOBJIPadDevice
などのサブクラスとおそらく特定のメソッドをoverrideするOBJIPhone5Device
のような、より深いサブクラスを持つOBJDevice
クラスを作成すると意味をなす。例えば、あなたのOBJDevice
はメソッドapplyRoundedCornersToView:withRadius:
を含むことができる。これは、デフォルトの実装です。しかし、具体的なサブクラスでオーバーライドすることができます。
サブクラス化することが、とても有益かもしれない別のケースは、モデルオブジェクトである。たいていの場合、私のモデルオブジェクトはisEqual:
,hash
,copyWithZone:
およびdescription
を実装するクラスを継承しています。これらのメソッドは、プロパティを反復適用することとミス起こし難くするため、一度だけ実装されます。(このようなbaseクラスを探しているなら、Mantleの使用を検討できます。Mantleはこれを正確にそしてさらに行えます。)
サブクラス化しない時
私が働いてきた多くのプロジェクトで、深いサブクラス階層を見てきました。私もこれと同じ罪を犯しています。階層が非常に浅くなければ、とても早く限界にぶつかる傾向があります。
幸いなことに、あなたがそのような深い階層を自分自身で見つける場合は、多くの代替案があります。以下のセクションで、より詳細に深堀します。サブクラスが、単に同じインターフェイスを共有している場合、プロトコルが、とても良い代替になり得ます。多くを変更する必要があるオブジェクトが分かっているならば、動的に、変化に対しデリゲートを使用して、それを設定したいかもしれません。いくつか単純な機能を持たせることにより、既存のオブジェクトを拡張する場合、カテゴリは一つの選択枝であるかもしれません。それぞれが、同じメソッドをオーバーライドするサブクラスの集合がある場合は、代わりに設定オブジェクトを使用することがあります。最後に、いくつかの機能を再利用したい場合、それらを拡張するのではなく、多様なオブジェクトを構成する方が良いかもしれません。
代替案
代替案:プロトコル
サブクラス化を使用する理由は、オブジェクトが特定のメッセージに応答することを確認したい時です。playerオブジェクトを持っているアプリを考えてみましょう。そして、それはビデオを再生できる。YouTubeのサポートを追加し、同じインターフェイスにしたいとする。しかし、実装は異なります。サブクラスでこれを達成する一つの方法は、以下のようになる。
@interface Player : NSObject
- (void)play;
- (void)pause;
@end
@interface YouTubePlayer : Player
@end
ほとんどの場合、2つのクラスは、多くのコード、まったく同じインタフェースを共有していません。このケースだと、代わりにプロトコルを使用することが良い解決策になるかもしれません。プロトコルを使用すると、次のようなコードを記述します。
@protocol VideoPlayer <NSObject>
- (void)play;
- (void)pause;
@end
@interface Player : NSObject <VideoPlayer>
@end
@interface YouTubePlayer : NSObject <VideoPlayer>
@end
この方法は、YouTubePlayer
はPlayer
の内部を知っている必要はありません。
代替案: デリゲーション
再び、上記の例のようなplayer
クラスがあるとします。今、ある場面で、あなたはplayでカスタムアクションを実行したいとします。これを行うことは、比較的容易である:カスタムサブクラスを作成し、play
メソッドをオーバーライドし、[super play]
をコールしそして、カスタムな作業を実行する。これは、それに対処するための一つの方法です。もう一つの方法は、プレイヤーオブジェクトを変更し、それにデリゲートを与えることです。例えば以下のように。
@class Player;
@protocol PlayerDelegate
- (void)playerDidStartPlaying:(Player *)player;
@end
@interface Player : NSObject
@property (nonatomic,weak) id<PlayerDelegate> delegate;
- (void)play;
- (void)pause;
@end
さて、プレイヤーのplay
メソッドで、デリゲートはplayerDidStartPlaying:
メッセージ送信を受け取る。このクラスを使用者は、今からサブクラスを有する代わりに、デリゲートプロトコルを実装することができる。そしてPlayer
オブジェクトは、とても一般的な状態にできます。これは、とても強力なテクニックで、Appleがそのフレームワークで豊富に使用しています。UITextField
のようなクラス、そしてNSLayoutManager
のようなクラスを考えてみてください。時々、分かれているプロトコルで、異なるメソッドを集めたい。例えば-delegate
だけでなく、dataSource
を持つ– UITableVIew
が行っているようなこと。
代替案: カテゴリ
ちょっとした機能追加でオブジェクトを拡張したいとする。メソッドarrayByRemovingFirstObject
を追加することで、NSArray
を拡張するとします。サブクラスを作る代わりに、これをカテゴリ実装することができます。次のように働きます。
@interface NSArray (OBJExtras)
- (void)obj_arrayByRemovingFirstObject;
@end
カテゴリを使用して、独自ではないクラスを拡張する場合、あなたのメソッドを先頭に付加すると良いでしょう。そうしないと、他の誰かが同じカテゴリ技術を使用して同じメソッドを実装するかもしれないという可能性があります。行動が一致しない場合そして、予想外のことが起こる可能性があります。
カテゴリを使うことの危険の一つは、大量のカテゴリをもつ羽目になり、概要を見失いうることである。このような場合、カスタムクラスを生成することが、恐らくより簡単である。
代替案: Configuration Objects
私が作り続けている誤りの一つ(今はかなり迅速に認識することができるが)は、いくらか抽象的な機能性を持ち、そして、多くサブクラスの一つの特定のメソッドをオーバーライドしているクラスが存在し続けていることである。例えば、プレゼンテーションアプリでは、Theme
クラスを持っているかもしれません。それは、backgroundColor
やfont
などの二つのプロパティとスライドでレイアウトするためのいくつかのロジックを持っています。
各々のテーマで、Theme
のサブクラスを作りメソッドをオーバーライドし(例えばsetup)、そしてプロパティを設定します。直接スーパークラスを使うことは、意味をなしません。このケースでは、構成オブジェクトを使用して、コードを少しシンプルにすることができます。Theme
クラス(例えばスライドレイアウト)に共有ロジックを維持する一方プロパティを持つだけの単純なオブジェクトに設定を移動できます。
例えば、ThemeConfiguration
クラスがbackgroundColorとフォントの特性を持っているでしょうし、Theme
クラスは初期化で、このクラスの値を取得します。
代替案: コンポジション
サブクラスに対する最も強力な代替案は、コンポジションである。存在しているが、同じinterfaceを共有しないコードの再利用をしたいならば、コンポジションは武器選択の一つになりうる。例えば、キャシュするクラスを設計しているとする。
@interface OBJCache : NSObject
- (void)cacheValue:(id)value forKey:(NSString *)key;
- (void)removeCachedValueForKey:(NSString *)key;
@end
サブクラス化で、これを行う容易な方法のひとつは、NSDictionary
をサブクラス化してdictionaryのメソッドを呼び、2つのメソッドを実装することである。
@interface OBJCache : NSDictionary
しかし、欠点がいくつかあります。dictionaryで実装されている事実は、詳細実装であるべきである。NSDictinary
のパラメータを期待するところでは、どこででもOBJCache
の値を提供できる。何かまったく異なるもの(例えば、独自のライブラリ)へ変更したい時は、いつでも多くのコードをリファクタリングする必要がある。
より良いアプローチは、このdictionaryをprivateプロパティ(またはインスタンス変数)で持ち、2つのcache
メソッドのみを公開することである。あなたはより多くの洞察力を得たので、実装変更に対する柔軟性を維持する。そして、あなたのクラスの使用者は、リファクタリングする必要がない。