|
|
Log In | Not a Member? |
Contact ADC |
| < Previous PageNext Page > |
適切に設計されたサブクラスはすべて、その祖先や役割に関係なく、いくつかの共通する特徴を持っています。設計が不十分なサブクラスは、エラーを起こしやすく、使用および拡張が難しく、パフォーマンスを低下させます。優れた設計のサブクラスは、これとはまったく逆になります。本稿では、効率性、堅牢性、利便性、そして再利用性を備えたサブクラスを設計するための推奨事項について説明します。
参考資料: 本稿ではサブクラスの作成に関していくつかの手法について触れていますが、説明の中心は基本的な設計に関することがらです。開発アプリケーションであるXcodeおよびInterface Builderを使用してクラス定義の一部を自動化する方法については説明しません。これらのアプリケーションの詳細については、『Xcode Quick Tour Guide』を参照してください。このセクションで説明している、設計に関することがらは、特にモデルオブジェクトに適用できます。『Model Object Implementation Guide』では、モデルオブジェクトの適切な設計と実装について詳しく説明しています。
サブクラス定義の形式
スーパークラスメソッドのオーバーライド
インスタンス変数
エントリポイントとエグジットポイント
初期化かデコードか
アクセサメソッド
キー値メカニズム
オブジェクトのインフラストラクチャ
エラー処理
リソース管理その他の効率向上手段
関数、定数、その他のCの型
クラスをパブリックにする場合
Objective-Cのクラスは、インターフェイスと実装で構成されています。慣例上、これら2つのクラス定義部分は別々のファイルに分けられます。(ANSI Cヘッダファイルと同様に)インターフェイスファイルは.hという拡張子を持ちます。実装ファイルは.mという拡張子を持ちます。通常、ファイル名のうち拡張子を除いた部分は、クラス名と同じです。たとえば、Controllerという名前のクラスでは、ファイル名は次のようになります。
Controller.h |
Controller.m |
インターフェイスファイルには、クラスのパブリックインターフェイスを確立する、メソッド(および関数)宣言のリストがあります。また、インスタンス変数、定数、文字列グローバル、その他のデータ型の宣言もあります。@interfaceディレクティブは、インターフェイスに必須の宣言の開始を示し、@endディレクティブは宣言の終了を示します。@interface ディレクティブは、クラスの名前と、直接の継承元クラスの名前を示すので特に重要です。形式は次のとおりです。
@interface ClassName : Superclass
Listing 3-2に、Controllerという架空のクラスのインターフェイスファイルを示します(宣言を加える前の状態)。
Listing 3-2 インターフェイスファイルの基本構造
#import <Foundation/Foundation.h> |
@interface Controller :NSObject { |
} |
@end |
クラスインターフェイスを完成するには、Figure 3-4に示すように、このインターフェイス構造の適切な場所に、必要な宣言を配置する必要があります。
これらの宣言の型の詳細については、“「インスタンス変数」”および“「関数、定数、その他のCの型」”を参照してください。
クラスインターフェイスでは、最初に、適切なCocoaフレームワークと、インターフェイスに出現するすべての型に対応するヘッダファイルをインポートします。#import プリプロセッサコマンドは、指定したヘッダファイルを取り込むという点では#includeに似ています。しかし、#importでは、コンパイル時に、以前に直接または間接にインクルードしていない場合にのみファイルをインクルードすることで効率を向上させています。コマンドの後に続く<...>によって、ヘッダファイルに対応するフレームワークを示し、スラッシュ文字の後がヘッダファイルを示します。#import行の構文は次のような形式になります。
#import < Framework / File .h>
Frameworkは、システムにおいてフレームワークのための標準の場所のいずれかになければなりません。Listing 3-2の例では、クラスのインターフェイスファイルの中で、FoundationフレームワークのFoundation.hファイルをインポートしています。この例のように、Cocoaの規則では、フレームワークと同じ名前を持つヘッダファイルには、フレームワークのすべてのパブリックインターフェイス(およびその他のパブリックヘッダ)をインクルードする一連の#importコマンドが含まれています。ちなみに、定義しようとしているクラスがアプリケーションの一部である場合は、CocoaアンブレラフレームワークのCocoa.hファイルをインポートするだけで済みます。
クラスの実装ファイルの構造は、Listing 3-3に示すようにもっと単純です。クラスの実装ファイルでは、最初にクラスのインターフェイスファイルをインポートすることが重要です。
Listing 3-3 実装ファイルの基本構造
#import "Controller.h" |
@implementation Controller |
@end |
メソッド実装はすべて、@implementationディレクティブから@endディレクティブまでの間に記述する必要があります。クラスに関係する関数の実装は、ファイルの任意の場所に記述できますが、慣例上、それらも2つのディレクティブの間に記述します。プライベート型の宣言(関数や構造体など)は、通常は#importコマンドと@implementationディレクティブの間に記述します。
カスタムクラスはその定義上、何らかのプログラム固有の方法によって、そのスーパークラスの動作を変更します。スーパークラスのメソッドをオーバーライドすることが、通常はこのような変更を達成する手段になります。サブクラスを設計する際には、オーバーライドするメソッドを特定し、それらをどのように実装しなおすかを考えるという手順が必要になります。
いくつかの一般的なガイドラインについては“「メソッドのオーバーライドを必要とする場合」”で説明していますが、どのスーパークラスメソッドをオーバーライドするかを特定するためには、クラスを詳しく調べる必要があります。クラスのヘッダファイルとドキュメントをよく読みます。また、スーパークラスの指定イニシャライザを探します。初期化を正常に行うには、クラスの指定イニシャライザでスーパークラスの指定イニシャライザを呼び出す必要があるためです(詳細については、“「オブジェクトの作成」”を参照してください)。
メソッドを特定したら、まず、(これは純粋に実践における手法なのですが)オーバーライドするメソッドの宣言を自分のインターフェイスファイルにコピーします。次に、同じ宣言を自分の.mファイルにコピーし、終端のセミコロンを閉じ中括弧に置き換えて、メソッドのスケルトン実装とします。
“「メソッドのオーバーライドを必要とする場合」”で説明したように、オーバーライドしたフレームワークメソッドは通常は(自分のコードからではなく)Cocoaフレームワークから呼び出されます。場合によっては、元のメソッドではなくオーバーライド版のメソッドを呼び出す必要があることを、フレームワークに知らせる必要があります。Cocoaではこのために各種の手段が用意されています。たとえば、Interface Builderアプリケーションでは、「Inspector」(情報)ウインドウで、(互換性のある)フレームワーククラスを自分のクラスに置き換えることができます。カスタムのNSCellクラスを作成した場合は、NSControl setCell:メソッドを使用して、特定のコントロールをクラスに関連付けることができます。
カスタムクラスを作成する理由は、スーパークラスの動作を変更するためだけでなく、プロパティを追加するためでもあります。ここでの“プロパティ”とは、サブクラスのインスタンスの属性と、インスタンスが持つほかのオブジェクトへの参照(つまり、関係)の両方を意味します。たとえば、Circleという架空のクラスがあったとします。図形に色を追加するサブクラスを作成する場合、そのサブクラスは何らかの方法で色の属性を持つ必要があります。色の属性を持つために、colorというインスタンス変数(おそらくNSColorオブジェクト型)をクラスインターフェイスに追加することが考えられます。カスタムクラスのインスタンスでは、この新しい属性がカプセル化され、特徴を示す永続的なデータとして保持されます。
Objective-Cのインスタンス変数は、クラス定義の一部であるオブジェクトや構造体その他のデータ型を宣言したものです。これらがオブジェクトの場合、オブジェクトは動的な型として宣言されるか(idを使用)、静的な型として宣言されるかのどちらかになります。両方の形式を次の例に示します。
id delegate; |
NSColor *color; |
一般に、オブジェクトのクラスのメンバが不定であるか、重要ではない場合は、オブジェクトインスタンス変数を動的な型として宣言します。インスタンス変数として保持されるオブジェクトは、親オブジェクトによってすでに保持されている場合(委任の場合と同様)を除いて、作成するか、コピーするか、または明示的に保持します。インスタンスを展開する場合は、オブジェクトインスタンス変数を initWithCoder:の中でデコードして割り当てを行っている間、インスタンス変数を保持しておく必要があります(オブジェクトのアーカイブと展開の詳細については、“「エントリポイントとエグジットポイント」”を参照してください)。
命名規則として、インスタンス変数の名前は句読点や特殊文字を含まない小文字の文字列にします。名前に複数の単語を含める場合は、それらをそのまま続けて記述しますが、2番目以降の各単語の頭文字を大文字にします。たとえば、次のようになります。
NSString *title; |
NSColor *backgroundColor; |
NSRange currentSelectedRange; |
インスタンス変数の宣言の前にIBOutletタグを付けた場合は、接続がnibファイルにアーカイブされている(と考えられる)アウトレットを示します。また、このタグによって、Interface BuilderとXcodeの動作の連携が可能になります。Interface Builderのnibファイルから展開されるアウトレットは、自動的に保持されます。
インスタンス変数は、オブジェクトのクライアントに供給できる、オブジェクトの属性以外のものも持つことができます。オブジェクトによって実行される何らかの処理の基礎として使用されるプライベートデータを、インスタンス変数に保持できる場合もあります。バッキングストアやキャッシュなどがその例です(データがインスタンスごとに固有ではなく、クラスのインスタンスどうしで共有する場合は、インスタンス変数ではなくグローバル変数を使用します)。
インスタンス変数をサブクラスに追加する際には、次に示すいくつかのガイドラインに留意します。
絶対に必要と思われるインスタンス変数だけを追加します。追加するインスタンス変数が多くなるほど、インスタンスのサイズが大きくなります。そして、作成するクラスのインスタンスが多くなるほど、この問題がより顕著になります。重要な値については、可能であれば、別のインスタンス変数を追加してその値を保持するのではなく、既存のインスタンス変数から算出します。
同様の効率上の理由から、クラスのインスタンスデータを効率的に表現するよう努めます。たとえば、いくつかのフラグをインスタンス変数として指定したい場合は、複数のブール値宣言ではなくビットフィールドを使用します(ただし、ビットフィールドではアーカイブが複雑になることに注意してください)。いくつかの関連する属性を、NSDictionaryオブジェクトを使用してキー値ペアとして統合することもできます。この場合、必ずキーを適切に文書化します。
インスタンス変数に、適切な有効範囲を与えます。変数に@publicの有効範囲を与えることは決してしないでください。カプセル化の原則に反することになります。クラスのサブクラスの候補(アプリケーションのクラスなど)がわかっていて、データへの効率的なアクセスを必要とする場合は、インスタンス変数に対して@protectedを使用します。それ以外の場合は、実装が高い次元で隠蔽される@privateを選ぶのが妥当です。フレームワークによって公開され、アプリケーションやほかのフレームワークによって使用されるクラスにとっては、この方法による実装の隠蔽は特に重要です。クラスの実装に変更を加えたときに、すべてのクライアントを再コンパイルする必要がないからです。
クラスの必要不可欠なプロパティとなるインスタンス変数を対象とするアクセサメソッドが存在することを確認します(アクセサメソッドは、インスタンス変数の値を取得または設定するものです)。この話題の詳細については、“「アクセサメソッド」”を参照してください。
自分のサブクラスをパブリックにする場合、つまり、ほかの開発者がそのサブクラスをさらにサブクラス化することが考えられる場合は、インスタンス変数リストの最後を予約済みフィールドで埋めます。予約済みフィールドは通常はidとして型定義します。この予約済みフィールドは、将来、別のインスタンス変数をクラスに追加する必要が生じた場合に、バイナリ上の互換性を保つのに役立ちます。パブリッククラスの準備の詳細については、“「クラスをパブリックにする場合」”を参照してください。
オブジェクトには、その存続期間の間、Cocoaフレームワークからさまざまな時点でオブジェクトメッセージが送信されます。ほとんどすべてのオブジェクト(クラスを含ます。クラス自体も実際にはオブジェクトです)は、実行時の存続期間の最初と、破棄の直前に、特定のメッセージを受信します。これらのメッセージによって呼び出されるメソッド(実装されている場合)は、その時点の関連する作業をオブジェクトが実行できるようにする“フック”となります。以下では、これらのメソッドを呼び出される順に示します。
initialize―クラスではこのクラスメソッドを使用して、クラスまたはクラスのインスタンスがほかのメッセージを受信する前に、自身を初期化できます。スーパークラスは、サブクラスよりも先にこのメッセージを受信します。古いアーカイブメカニズムを使用しているクラスでは、initializeを実装して自身のクラスバージョンを設定できます。そのほか、initializeは、何らかのサービスにクラスを登録する場合や、すべてのインスタンスで使用される何らかのグローバルな状態を初期化する場合にも使用します。ただし、この種の処理はinitializeで実行するよりも、インスタンスメソッドで、遅らせて(つまり、必要になった時点で初めて)実行したほうがよい場合もあります。
init (または他のイニシャライザ)―スーパークラスの指定イニシャライザによる処理がクラスにとって十分な場合を除き、インスタンスの状態を初期化するためにinitまたはその他の何らかのイニシャライザを実装する必要があります。指定イニシャライザなどのイニシャライザの詳細については、“「オブジェクトの作成」”を参照してください。
initWithCoder:―たとえば、自分のクラスがモデルクラスの場合など、自分のクラスのオブジェクトがアーカイブされることを前提とする場合は、NSCodingプロトコルを採用し(必要な場合)、その2つのメソッド(initWithCoder:および encodeWithCoder:)を実装します。. これらのメソッドではそれぞれ、オブジェクトのインスタンス変数をアーカイブに適した形式でデコードおよびエンコードする必要があります。この目的のために、NSCoderのデコード用およびエンコード用のメソッドを使用したり、キーアーカイブ機能(NSKeyedArchiverおよびNSKeyedUnarchiver)を使用したりできます。オブジェクトが明示的に作成されるのではなく展開されようとしているときは、イニシャライザではなくinitWithCoder:メソッドが呼び出されます。このメソッドでは、インスタンス変数の値をデコードした後に、それらの値を該当する変数に割り当て、必要に応じて保持またはコピーします。この話題の詳細については、『Archives and Serializations Programming Guide for Cocoa』を参照してください。
awakeFromNib―アプリケーションでnibファイルをロードすると、アーカイブからロードされた各オブジェクトにawakeFromNibメッセージが送信されますが、送信されるのは対象オブジェクトがメッセージに応答できる場合で、なおかつ、アーカイブ内のすべてのオブジェクトのロードと初期化が完了した後のみです。awakeFromNibメッセージを受信したオブジェクトは、そのすべてのアウトレットインスタンス変数が設定されることが保証されます。通常、nibファイルを所有しているオブジェクト(ファイルの所有者)がawakeFromNibを実装し、アウトレットとターゲット/アクションの接続の設定を必要とする初期化処理を実行します。
encodeWithCoder:―このメソッドは、クラスのインスタンスをアーカイブする必要がある場合に実装します。このメソッドは、オブジェクトの破棄の直前に呼び出されます。詳細については、前述のinitWithCoder:の説明を参照してください。
dealloc―このメソッドは、インスタンス変数を解放し、自分のクラスのインスタンスで確保したほかのメモリの割り当てをすべて解除するために実装します。このメソッドが制御を戻した直後に、インスタンスが破棄されます。deallocメソッドの詳細については、“「オブジェクトの作成」”を参照してください。
アプリケーションのグローバルアプリケーションオブジェクト(NSAppとして識別されます)もまた、オブジェクトがNSAppのデリゲートで、適切なメソッドを実装している場合に、アプリケーションの存続期間の最初と最後に、オブジェクトにメッセージを送信します。アプリケーションの起動の直後に、NSAppはメッセージとしてapplicationWillFinishLaunching:およびapplicationDidFinishLaunching:を自身のデリゲートに送信します(前者は、ダブルクリックされたドキュメントが開く前に、後者はドキュメントが開いた直後に、それぞれ送信されます)。どちらのメソッドにおいても、アプリケーションの実行時の存続期間における早い段階で1回だけ実行する必要のある、グローバルなアプリケーションロジックをデリゲートの中で指定できます。アプリケーションの終了の直前に、NSAppはapplicationWillTerminate:を自身のデリゲートに送信します。デリゲートでは、ドキュメントやアプリケーションの状態を保存することによって、プログラムの終了を適切に処理するためのメソッドを実装できます。
このほか、Cocoaフレームワークには、ほかにウインドウのクローズからアプリケーションのアクティブ化および非アクティブ化にいたるさまざまなイベントに対応したフックが多数用意されています。通常、これらは委任メッセージとして実装します。また、オブジェクトをフレームワークオブジェクトのデリゲートにして、必要なメソッドを実装する必要があります。また、フックが通知である場合もあります(委任の詳細については、“「オブジェクトとの通信」”を参照してください。
作成するクラスのオブジェクトをアーカイブおよび展開させる場合、クラスはNSCodingプロトコルに準拠する必要があります。オブジェクトをエンコードするメソッド(encodeWithCoder:)とオブジェクトをデコードするメソッド(initWithCoder:)をクラスに実装する必要があります。アーカイブから作成されるオブジェクトの初期化には、1つまたは複数のイニシャライザメソッドではなく、initWithCoder:メソッドが呼び出されます。
クラスのイニシャライザメソッドとinitWithCoder:はほとんど同じ処理を実行する可能性があるため、共通の処理を、イニシャライザとinitWithCoder:の両方から呼び出されるヘルパーメソッドに集めるのは意味があります。たとえば、セットアップルーチンの一部として、オブジェクトでドラッグの種類とドラッグ元を指定する場合、オブジェクトにListing 3-4に示すようなヘルパーメソッドを実装することが考えられます。
Listing 3-4 初期化ヘルパーメソッド
(id)initWithFrame:(NSRect)frame { |
self = [super initWithFrame:frame]; |
if (self) { |
[self setTitleColor:[NSColor lightGrayColor]]; |
[self registerForDragging]; |
} |
return self; |
} |
- (id)initWithCoder:(NSCoder *)aCoder { |
self = [super initWithCoder:aCoder]; |
titleColor = [[aCoder decodeObject] copy]; |
[self registerForDragging]; |
return self; |
} |
- (void)registerForDragging { |
[theView registerForDraggedTypes: |
[NSArray arrayWithObjects:DragDropSimplePboardType, NSStringPboardType, |
NSFilenamesPboardType, nil]]; [theView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; } |
クラスのイニシャライザとinitWithCoder:はこのように同等の役割を果たせますが、例外もあります。フレームワーククラスのカスタムサブクラスを作成し、そのインスタンスがInterface Builderパレットに出現する場合です。通常、そのような場合は、プロジェクトでクラスを定義し、オブジェクトをInterface Builderパレットからインターフェイスにドラッグした後、Interface Builderの情報ウインドウの「Custom Class」ペインを使用して、そのオブジェクトをカスタムサブクラスに関連付けます。ただし、この場合、サブクラスのinitWithCoder:メソッドが展開中に呼び出されることはなく、代わりにinitメッセージが送信されます。カスタムオブジェクトの特別な設定処理は awakeFromNibの中で実行してください。このメソッドは、nibファイルのすべてのオブジェクトの展開が完了した後に呼び出されます。
アクセサメソッド(または単に“アクセサ”)は、インスタンス変数の値を取得または設定するもので、適切に設計されたクラスのインスタンスにおいて必要不可欠な要素です。アクセサメソッドはオブジェクトのプロパティへの門のような機能を果たし、プロパティへのアクセスを制限することでオブジェクトのインスタンスデータのカプセル化を実現します。
規則上の理由から、また、クラスをキー値コーディング(“「キー値メカニズム」”を参照)に準拠させるために、アクセサメソッドには特定の形式の名前を付ける必要があります。インスタンス変数の値を返すメソッド(しばしばgetterと呼ばれます)の名前は、単純にインスタンス変数の名前です。インスタンス変数の値を設定するメソッド(setter)の名前は、“set”で始まり、その直後にインスタンス変数の名前が付きます(1文字目を大文字にします)。たとえば、“color”という名前のインスタンス変数の場合、getterおよびsetterアクセサの宣言は次のようになります。
- (NSColor *)color; |
- (void)setColor:(NSColor *)aColor; |
インスタンス変数がintやfloatなどのCのスカラー型の場合、アクセサメソッドの実装は非常に単純になる傾向があります。たとえば、名前がcurrentRateで型がfloatのインスタンス変数の場合、アクセサメソッドの実装方法はListing 3-5に示すようなものになります。
Listing 3-5 非オブジェクトインスタンス変数の場合のアクセサの実装
- (float)currentRate { |
return currentRate; |
} |
- (void)setCurrentRate:(float)newRate { |
currentRate = newRate; |
} |
インスタンス変数にオブジェクトを保持する場合は、状況がもう少し複雑になります。これらのオブジェクトはインスタンス変数であるため、永続的なものにする必要があります。したがって、割り当て時に作成、コピー、または保持する必要があります。setterアクセサでインスタンス変数の値を変更した場合、setterアクセサでは永続性を確保するだけでなく、古い値を適切に破棄する必要もあります。getterアクセサは、インスタンス変数の値を要求側のオブジェクトに供給します。オブジェクトの所有権に関するCocoaのポリシーから導かれる次の2つの前提に立つと、どちらの種類のアクセサの操作も、メモリ管理に影響を及ぼします。
メソッドから返されるオブジェクト(getterアクセサから返されるオブジェクトなど)は、呼び出し元のオブジェクトの有効範囲内で有効である。つまり、その有効範囲内では、オブジェクトが解放されたり、オブジェクトの値が変更されたりしないことが保証される(特に断りのない限り)。
呼び出し元のオブジェクトでは、アクセサなどのメソッドからオブジェクトを受け取ったときに、最初にそのオブジェクトを明示的に保持(またはコピー)しない限り、オブジェクトを解放してはならない。
この2つの前提を念頭に置きつつ、titleという名前のNSStringインスタンス変数について、そのgetterおよびsetterアクセサの実装例を2つ考えてみましょう。まず、Listing 3-6を示します。
Listing 3-6 オブジェクトインスタンス変数用のアクセサの実装—良い例
- (NSString *)title { |
return title; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title autorelease]; |
title = [newTitle copy]; |
} |
} |
getterアクセサは、単純にインスタンス変数への参照を返すだけです。一方、setterアクセサはgetterアクセサよりも処理が多くなります。インスタンス変数に対して渡された値が現在の値と同じでないことを確かめた後、現在の値を自動解放してから、新しい値をインスタンス変数にコピーします(autoreleaseをオブジェクトに送信するほうが、releaseを送信するよりも“スレッドセーフ”になります)。ただし、この手法では依然として潜在的な危険が伴います。getterアクセサから返されたオブジェクトをクライアントで使用している間に、setterアクセサが古いNSStringオブジェクトを自動解放し、その直後にオブジェクトが解放され破棄された場合はどうなるでしょう。その場合、クライアントオブジェクトが持つインスタンス変数への参照が有効でなくなります。
Listing 3-7に示すもう1つのアクセサメソッドの実装例では、getterメソッドの中でインスタンス変数の値を保持し、それから自動解放することによって、この問題を回避しています。
Listing 3-7 オブジェクトインスタンス変数用のアクセサの実装—より良い例
- (NSString *)title { |
return [[title retain] autorelease]; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title release]; |
title = [newTitle copy]; |
} |
} |
どちらの例(Listing 3-6およびListing 3-7)でも、setterメソッドで新しいNSStringインスタンス変数を保持するのではなく コピーしています。なぜ保持しないのでしょう。一般的な規則として、インスタンス変数に代入されたオブジェクトが値オブジェクトの場合、つまり、文字列や日付、数値、企業レコードなどの何らかの属性を表すオブジェクトの場合は、コピーします。属性値を保持することに関心があり、自分の知らないところで変異させられる危険は避けることが目的です。つまり、オブジェクトの独自のコピーを持つのです。
一方、保存およびアクセスの対象となるオブジェクトが、NSViewやNSWindowオブジェクトなどのエンティティオブジェクトの場合、それを保持する必要があります。エンティティオブジェクトはより集約的でリレーショナルなため、それらをコピーすると負担が大きくなる可能性があるからです。オブジェクトが値オブジェクトかエンティティオブジェクトかを決めるには、オブジェクトの値に注目しているのか、それともオブジェクトそのものに注目しているのかを判断するのが1つの方法です。注目の対象が値の場合は、おそらく値オブジェクトであり、コピーしてください(もちろん、オブジェクトがNSCopyingプロトコルに準拠していることが前提です)。
setterメソッドでインスタンス変数を保持するかコピーするかを決める別の方法として、インスタンス変数が属性なのか関係なのかを調べるという方法があります。この方法は、アプリケーションのデータを表すオブジェクトであるモデルオブジェクトの場合に特に有効です。属性とは、本質的には値オブジェクトと同じであり、たとえば色(NSColorオブジェクト)やタイトル(NSStringオブジェクト)など、属性をカプセル化しているオブジェクトを定義する特性としてのオブジェクトです。一方、関係とは、1つまたは複数のほかのオブジェクト(またはそれらのオブジェクトへの参照)との間の関係のことです。通常、setterメソッドでは、属性の値はコピーし、関係は保持します。ただし、関係には濃度の概念があり、1対1または1対多になる可能性があります。1対多の関係は、通常はNSArrayやNSSetのインスタンスなどのコレクションオブジェクトによって表現されますが、その場合、単にインスタンス変数を保持すること以外に何らかの処理をsetterメソッドで実行しなければならないことがあります。詳細については、『Model Object Implementation Guide』を参照してください。属性または関係としてのオブジェクトプロパティの詳細については、“「キー値メカニズム」”を参照してください。
クラスのsetterアクセサをListing 3-6またはListing 3-7に示すように実装した場合、クラスのdeallocメソッドでインスタンス変数の割り当てを解除するには、引数にnilを指定して適切なsetterメソッドを呼び出すだけで済みます。
キー値バインディング、キー値コーディング、キー値監視など、名称に“キー値”を含むいくつかのメカニズムはCocoaの基礎的な構成要素です。これらは、たとえばバインディング(オブジェクト間で値のやり取りや値の同期を自動的に行う技術)など、Cocoaの技術に欠かせない要素です。また、これは、アプリケーションをスクリプト対応にする(つまり、AppleScriptのコマンドに対応できるようにする)ためのインフラストラクチャの少なくとも一部を提供します。特に、キー値コーディングとキー値監視は、カスタムのサブクラスを設計するうえで重要な考慮事項です。
“キー値”という言葉は、プロパティの名前を、その値を取得するためのキーとして使用する技術を指します。この言葉はオブジェクトモデリングパターンの用語の一部です。オブジェクトモデリングパターンは、リレーショナルデータベースの記述に使用されるエンティティ-関係モデリングから派生しています。オブジェクトモデリングでは、オブジェクト(特にModel-View-Controllerパターンの、データを持つモデルオブジェクト)がプロパティを持ちますが、それらのプロパティは通常は(必ずではありませんが)インスタンス変数の形式をとります。プロパティは、名前や色などの属性か、あるいは1つまたは複数のほかのオブジェクトへの参照です。これらの参照のことを関係と呼び、関係は1対1または1対多になる可能性があります。プログラム内のオブジェクトのネットワークは、オブジェクトどうしの相互の関係を通じてオブジェクトグラフを形成します。オブジェクトモデリングでは、キーパス(ドットで区切られたキーからなる文字列)を使用して、オブジェクトグラフの中で関係をたどり、オブジェクトのプロパティにアクセスすることができます。
注: オブジェクトモデリングの詳細については、“「オブジェクトモデリング」”を参照してください。
キー値バインディング、キー値コーディング、およびキー値監視は、このトラバーサルを可能にするメカニズムです。
キー値バインディング(KVB)は、オブジェクト間のバインディングを確立し、それらのバインディングの削除とアドバタイズも行います。キー値バインディングではいくつかの簡易プロトコルが利用されます。プロパティのバインディングでは、オブジェクトと、プロパティへのキーパスを指定する必要があります。
キー値コーディング(KVC)は、 NSKeyValueCoding簡易プロトコルの実装を通じて、オブジェクトのアクセサメソッドを直接呼び出さなくてもキーを使用してそのオブジェクトのプロパティの値を取得および設定できるようにします(Cocoaにはプロトコルのデフォルト実装が用意されています)。キーは通常、アクセス対象オブジェクトのインスタンス変数やアクセサメソッドの名前に対応しています。
キー値監視(KVO)は、 NSKeyValueObserving簡易プロトコルの実装を通じて、オブジェクトがほかのオブジェクトのオブザーバとして自分自身を登録できるようにします。監視対象のオブジェクトは、自身のプロパティの1つが変更されたときに、そのことをオブザーバに直接通知します。Cocoaでは、KVO準拠のオブジェクトの各プロパティに対して自動オブザーバ通知機能が実装されています。
サブクラスの各プロパティをキー値コーディングの要件に準拠させるには、次を実行します。
keyという名前の属性または対一関係の場合は、keyという名前のアクセサメソッド(getter)と、set Key :という名前のアクセサメソッド(setter)を実装します。たとえば、salaryという名前のプロパティの場合、アクセサメソッドの名前はsalaryおよびsetSalary:になります。
対多関係の場合は、プロパティが基づいているインスタンス変数が、コレクション(NSArrayオブジェクトなど)、またはコレクションを返すアクセサメソッドであれば、getterメソッドにプロパティと同じ名前を付けます(たとえばemployeesなど)。プロパティが可変であるものの、getterメソッドが可変コレクション(NSMutableArrayなど)を返さないのであれば、insertObject:in Key AtIndex:およびremoveObjectFromKeyAtIndex:を実装する必要があります。インスタンス変数がコレクションではなく、getterメソッドがコレクションを返さないのであれば、NSKeyValueCodingのほかのメソッドを実装する必要があります。
自動オブザーバ通知機能で十分な場合は、オブジェクトがKVC準拠であることを確認するだけで、オブジェクトがKVO準拠になります。ただし、キー値監視を手動で実装することもできます。その場合はさらに作業が必要になります。
参考資料: これらの技術の詳細については、『Key-Value Coding Programming Guide』および『Key-Value Observing Programming Guide』を参照してください。KVC、KVO、KVBを使用したCocoaバインディング技術の詳細については、“「オブジェクトとの通信」”の“「バインディング」”を参照してください。
サブクラスの設計が適切であれば、そのクラスのインスタンスはCocoaオブジェクトに求められているとおりに動作します。そのオブジェクトを使用するコードでは、オブジェクトをクラスのほかのインスタンスと比較したり、オブジェクトの内容を(デバッガなどで)検出したりするなど、オブジェクトを対象に同様の基本的な操作を実行できます。
カスタムのサブクラスでは、次に示すルートクラスメソッドと基本プロトコルメソッドの大部分(必ずしもすべてではありませんが)を実装することが求められます。
isEqual:およびhash―比較操作にオブジェクト固有のロジックを組み込むには、NSObjectのこれらのメソッドを実装します。たとえば、クラスのインスタンスがそのシリアル番号によって個別化される場合は、シリアル番号を比較の基準にします。詳細については、“「イントロスペクション」”を参照してください。
description―オブジェクトのプロパティや内容を簡潔に示す文字列を返すには、NSObjectのこのメソッドを実装します。この情報はgdbデバッガではprint objectコマンドによって返され、書式付き文字列の中でオブジェクトの%@指定子によって使用されます。たとえば、Employeeという架空のクラスがあり、名前、雇用日、部署、役職IDの各属性を持つとします。このクラスのdescriptionメソッドはたとえば次のようになります。
- (NSString *)description { |
return [NSString stringWithFormat:@”Employee:Name = %@, |
Hire Date = %@, Department = %@, Position = %i\n”, [self name], |
[[self dateOfHire] description], [self department], |
[self position]]; |
} |
copyWithZone:―作成するクラスのクライアントによってクラスのインスタンスがコピーされることを前提とする場合は、NSCopying プロトコルのこのメソッドを実装します。通常は、モデルオブジェクトを含め、値オブジェクトがコピーの候補になります。NSWindowやNSColorPanelなどのオブジェクトはコピーの候補にはなりません。クラスのインスタンスが可変の場合は、代わりにNSMutableCopyingプロトコルに準拠してください。
initWithCoder:および encodeWithCoder:―作成するクラスのインスタンスがアーカイブされることを前提とする場合(モデルクラスの場合など)は、NSCodingプロトコルを採用し(必要な場合)、これら2つのメソッドを実装します。NSCodingのメソッドの詳細については、“「エントリポイントとエグジットポイント」”を参照してください。
作成するクラスのいずれかの祖先が正式プロトコルを採用している場合は、自分のクラスもそのプロトコルに正しく準拠していることを保証する必要があります。つまり、スーパークラスにおけるいずれかのプロトコルメソッドの実装が自分のクラスにとって適切でない場合は、それらのメソッドを自分のクラスで実装しなおします。
プログラマがエラーを適切に処理しなければならないことは、どのようなプログラミングの原則においても自明なことです。しかし、多くの場合、何が“適切”かは、プログラミング言語やアプリケーション環境その他の要因によって異なります。Cocoaでは、サブクラスのコードでエラーを処理する場合の一連の規則と対処法が独自に設けられています。
メソッド実装の中で発生したエラーがシステムレベルエラーまたはObjective-Cランタイムエラーの場合は、必要に応じて例外を作成して発生させ、可能であればエラーをローカルに処理します。
通常、Cocoaでは、プログラミング上のランタイムエラーや予期しないランタイムエラーに対応するために例外を使用します。このようなエラーには、コレクションへの範囲外アクセス、不変オブジェクトを可変にしようとする試み、無効なメッセージの送信、ウインドウサーバへの接続の喪失などがあります。これらのエラーについては、通常は実行時にではなく、アプリケーションの作成段階で例外を使用して対処します。Cocoaでは、例外ハンドラを使用してキャッチできるいくつかの例外があらかじめ定義されています。あらかじめ定義されている例外と、例外の生成および処理のための手順とAPIの詳細については、『Exception Programming Topics for Cocoa』を参照してください。
それ以外のエラー(予期しうるランタイムエラーを含む)については、nil、NO、NULL、または0を表すその他の適切な型形式を、呼び出し側に返します。このようなエラーには、ファイルの読み取りや書き込みができない、オブジェクトの初期化に失敗した、ネットワーク接続を確立できない、コレクション内でのオブジェクトの検索に失敗した、などがあります。エラーに関する補足情報を送信側に返す必要があると思われる場合は、NSErrorオブジェクトを使用します。
NSErrorオブジェクトでは、エラーコード(Mach、POSIX、またはOSStatusに固有の場合もある)およびプログラム固有の情報の辞書など、エラーに関する情報がカプセル化されています。直接返される否定の値(nilやNOなど)を、エラーを示す基本的なインジケータとします。より具体的なエラー情報を伝える場合には、メソッドの引数の中でNSErrorオブジェクトを間接的に返します。
ユーザの判断や操作を必要とするエラーの場合は、警告ダイアログを表示します。
警告ダイアログを表示し、ユーザの応答を処理するには、NSAlertクラス(および関連する機能)のインスタンスを使用します。詳細については、『Dialogs and Special Panels』を参照してください。
NSErrorオブジェクト、エラー処理、およびエラー警告の表示の詳細については、『Error Handling Programming Guide For Cocoa』を参照してください。
オブジェクトのパフォーマンスはさまざまな手段を講じて向上できます。もちろん、これらのオブジェクトを組み込んで管理するアプリケーションについても、同じ手段を通じてパフォーマンスを向上できます。そのための手順や原則は、マルチスレッド処理から、描画の最適化、コードのメモリ使用量を減らすための手法など、多岐にわたります。これらの手法の詳細については、『Cocoa Performance Guidelines』や、パフォーマンスに関するその他の文書を参照してください。
しかし、次に示す3つの単純かつ常識的なガイドラインに従うことによって、より高度な手法を採用しなくても、オブジェクトのパフォーマンスを大幅に向上させることができます。
実際に必要になるまで、リソースをロードしたりメモリを割り当てたりしない。
nibファイルや画像などのプログラムのリソースをロードしても、それらをずっと後になってから使用したり、またはまったく使用しないというのでは、まったく非効率です。正当な理由もなく、プログラムのメモリ領域が増えることになります。リソースのロードやメモリの割り当ては、すぐに必要とする時点に至ったときに初めて行ってください。
たとえば、アプリケーションの環境設定ウインドウが別のnibファイルにある場合は、ユーザが初めてアプリケーションメニューから「環境設定」を選ぶまでは、そのファイルをロードしないでください。何らかの処理のためにメモリを割り当てる場合にも同様の注意が必要です。そのメモリが実際に必要になるまで、割り当てを控えてください。このような遅延ロードまたは遅延割り当ての手法は容易に実装できます。たとえば、アプリケーションのユーザインターフェイスに表示するための画像を、ユーザが初めて要求した時点でロードするとします。Listing 3-8では、画像用のgetterアクセサで画像をロードするという手法を使用しています。
Listing 3-8 リソースの遅延ロード
- (NSImage *)fooImage { |
if (!fooImage) { // fooImageがインスタンス変数の場合 |
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"Foo" ofType:@"jpg"]; |
if (!imagePath) return nil; |
fooImage = [[NSImage alloc] initWithContentsOfFile:imagePath]; |
} |
return fooImage; |
} |
CocoaのAPIを使用し、それよりも下位レベルのプログラミングインターフェイスにまで掘り下げることはしない。
Cocoaフレームワークでは、その堅牢性、セキュリティ、効率をできる限り高めるために、数多くの努力がなされています。加えて、これらの実装では、開発者に認識されない可能性のある相互依存性が管理されています。CocoaのAPIを使用せずに、より下位レベルのインターフェイスを使用した独自の解決策に頼った場合、おそらく、記述するコードの量が増え、誤りや非効率の危険性を高めることになります。また、Cocoaを利用すれば、将来の機能強化を利用するための備えがより万全になり、同時に、基盤の実装に加えられる変更の影響からも隔離されます。したがって、プログラミング作業においてはCocoaによる代わりの手段が存在するなら、その手段を使用してください。たとえば、開いているファイルをひととおりシークする場合は、BSDのルーチンではなく、NSFileHandleクラスを使用してください。また、描画の場合も、Quartz (Core Graphics)の関数を呼び出すのではなくNSBezierPathメソッドとNSColorメソッドを使用してください。
カスタムオブジェクトの効率と堅牢性を向上させるために実行できる最も重要なことは、適切なメモリ管理手法を実践する、ということに尽きるでしょう。すべてのオブジェクトの割り当て、copy、またはretainについて、対応するreleaseが存在することを確かめてください。メモリ管理のための適切なポリシーや手法を習得してください。後はひたすら実践あるのみです。詳細については、『Memory Management Programming Guide for Cocoa』を参照してください。
Objective-CはANSI Cのスーパーセットであるため、関数、typedef構造体、enum定数、マクロといった、任意のCの型をコードで使用できます。設計上の重要な問題として、カスタムクラスのインターフェイスと実装の中で、どのような場合に、どのような方法で、これらの型を使用するのか、ということがあります。
カスタムクラスの定義におけるCの型の使用に関して、いくつかのガイドラインを次に示します。
要求される頻度が高いものの、サブクラスによるオーバーライドの必要がない機能については、メソッドではなく関数を定義します。これはパフォーマンス上の理由によるものです。このような場合は、関数をクラスAPIの一部にするのではなく、プライベートにするのが最善です。また、どのクラスにも関連付けられない動作の場合や(グローバルであるため)、単純な型(Cのプリミティブや構造体)を対象とする操作の場合にも、関数を実装できます。ただし、グローバルな機能については、拡張性の理由から、クラスを作成し、そこからシングルトンインスタンスを生成するほうが適切な場合もあります。
次の条件に適合する場合は、単純なクラスではなく構造体型を定義します。
フィールドのリストの拡張を前提としない場合。
すべてのフィールドがパブリックの場合(パフォーマンス上の理由による)。
動的なフィールドが存在しない場合(動的なフィールドでは保持や解放などの特別な処理が必要になることがある)。
サブクラス化などのオブジェクト指向の手法を利用するつもりがない場合。
これらの条件がすべて適合する場合でも、Objective-Cで構造体を使用するかどうかは主にパフォーマンスによって判断してください。言い換えれば、パフォーマンス上の十分な理由がない場合は、単純なクラスを使用したほうが適切です。
定数は、#defineで定義するよりは、enumとして宣言します。後者のほうが型定義に適しており、デバッガでそれらの値を見つけることができます。
アプリケーションのコントローラクラスなど、自分で使用するカスタムクラスを定義する場合は、クラスについて熟知しており、必要な場合にいつでも設計しなおせるので、高い柔軟性が得られます。しかし、カスタムクラスがほかの開発者のスーパークラス、つまり、パブリッククラスになる可能性が高い場合には、自分のクラスに対する他の開発者の想定を考慮して、より慎重な設計を行う必要があります。
Cocoaのパブリッククラスを開発する際のガイドラインをいくつか示します。
予約インスタンス変数を1つまたは2つ追加します。これらは、クラスの将来のバージョンとのバイナリ上の互換性を保つのに役立ちます(“「インスタンス変数」”を参照)。
供給するオブジェクトのメモリ管理を適切に処理するアクセサメソッドを実装します(“「アクセサメソッド」”を参照)。クラスのクライアントに対して、getterアクセサから、自動解放されるオブジェクトが返されるようにします。
Cocoaのパブリッククラスに関して『Coding Guidelines for Cocoa』で推奨されている命名のガイドラインに従います。
| < Previous PageNext Page > |
Last updated: 2006-05-23
|
Get information on Apple products.
Visit the Apple Store online or at retail locations. 1-800-MY-APPLE Copyright © 2007 Apple Inc. All rights reserved. | Terms of use | Privacy Notice |