もくじ へ

2. Cocoa の味見

 それでは、わたしが「なるほど」と思うきっかけになった「味見ソフト」を紹介しましょう。
 「Project Builder」の使い方や基本的なことはCocoaはヤッパリ!のONLINEの記事の「3.割り勘計算ソフトを作ろう」くらいまでは読んでいて、概略知っていることを前提としてこの先の話を進めます。

設 計

 どんな「ちょっとしたプログラム」でも、何をしたいかが明確で無ければ始められない。プログラミングとは自分の「思想」や「意志」を記号化する作業だからである。
 このサンプルプログラムの場合、見た目は以下のようになるものとする。


 上の四角に数字を入れて、ボタンを1回クリックすると、その数字で初期化されたモデルが生成され(モデルが生成され..などとかしこまって言ったところで、変数1個のモデルだが)、2回め以降ボタンをクリックすると、そのモデルに上の四角に書かれた数字が足し込まれて行く。そんなモデルを考えて見る。下の四角は、モデルの内容のモニター窓とする。
 自分だけが「ちょっと使ってみる」ソフトだから、「わけの解らない他人が変な使い方をしたらどうなるか?」と言う検討は抜きにする。また「メモリーリーク」に関しても、プログラムを終了すれば全てがちゃんとご破算になるわけだから、この対応も抜きにする。
 その代わりと言うのも変だが、あえてModel - View - Controllerの設計(Cocoaはヤッパリ!参照)にしてみる。これは、「インスタンス化」と言う、私にとって最も勘所の掴めなかった概念を明確にするためである。このサンプルは、実はそのために作ったものだったのです。

コーディングの前に

 以前、C言語を使った時にも、必要なメモリーは配列に定義して最初から確保するような単純なソフトしか書いたことがないので、プログラムが起動された後で「インスタンス化」によってオブジェクトがメモリーに展開されると言う概念が、どうもピンと来ない。「Interface Builder」でコロッとインスタンス化して使う部分は、最初からインスタンス化されているので気にしなくて良い訳だが、Model - View - Controllerの設計でのModel部分をどうするかが関心事となる。「Interface Builder」でコロッとインスタンス化して、線で繋いで使うこともできるが、どうもそれでは納得が行かない。Model部のファイルは、「Interface Builder」とは全く無縁に、Controller部の .mファイルに何か書き込むだけでインスタンス化されて呼び出されるべきと考えてしまう。なんで?と問われても戸惑うが、ソフトウエアの部品化、再利用性の見地からすると、おおもとのObjectiveCの作者がそう作った筈だと確信している。
 ソフトウエアの部品化とか再利用性とか言うことは、大規模なソフトウエアを商品として開発している会社だけに必要なことではない。むしろ、個人でちょっとしたプログラムを作って利用することを日常としている場合こそ、以前に作ったソフトから、バグの無い動作確認済みの「部分」をコピペして利用することで生産性を上げることが重要で、それでこそ趣味に割ける短い時間のなかで、より深い楽しみを味わえると言うものである。

インスタンス化しないのもアリ

 極端な例だが、この例ではあえてインスタンス化せずに、Model部はクラスのままで実行している。こんなことをしても(後で示すように)ちゃんとインスタンス化した場合と基本形式は変わらないので、後からやっぱりインスタンス化しようと考えを変えたとしても、十分に再利用性がある。非常に短いソフトだが、いろいろと試行錯誤の末たどり着いたもので、参考になると思う。もちろん、このままでは全く拡張性はないし、決してこう言うプログラミング法が良いのだと主張するつもりはないが、「ちょっとしたプログラム」の場合には、ObjectiveCの見やすさを保ったままで短く書けるメリットがあるとも言える。

「Model部」のコーディング

 「Interface Builder」は、インスタンス化や配線をしない場合でも、コードのひな形を作成するためのツールとして便利に利用させてもらう。NSObjectのサブクラスにMyModelObjectを作成し、形式的にアクションを3つ加えてコード化する。アクションの名前は、使用予定のメソッド名にしておくと更に後での書き直し量が減る。
 下に示すのが書き直し後の MyModelObject.h ファイルである。ここでは全てのメソッドを + から始まる「クラスメソッド」にしてしまったが、2つ以上のインスタンスを作る予定が無いなら、インスタンス化しないでも実行できる「クラスメソッド」でも動作が可能である。
 「Interface Builder」に登録されているクラスの仕様と、書き直してしまったコードとが整合しないことが気になるなら、書き直した .h ファイルを「Interface Builder」の MainMenu.nibウインドウの中にドラッグして放り込めば、「Interface Builder」がこれを認識して、登録されているクラスの仕様を直してくれる。


/* MyModelObject */

#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
}
+ (void)initWith:(double)sender;
+ (void)addData:(double)sender;
+ (double)lookData;
@end
    

 次が MyModelObject.m ファイル。あまりにも分りやすいので説明の必要はないと思う。


#import "MyModelObject.h"

@implementation MyModelObject

double sumData;

+ (void)initWith:(double)sender
{
    sumData = sender;		// 初期値のセット
}

+ (void)addData:(double)sender
{
    sumData += sender;
}

+ (double)lookData
{
    return sumData;
}

@end
    

 細かく言うと、クラスの変数の実装に関しては、他のファイルからの隠蔽性を考慮して static 宣言すべきと言うような話もあるが、ちょっとしたプログラムなので、そこまで考えないことにする。

「View部」の作成

 「Interface Builder」でさささっと作るだけある。実際に「ちょっとしたプログラム」の場合には、ウインドウ上にコメントを書く必要など無いが、忘れた頃に将来書き直す可能性があるならば、配線の状況に付いてコメントしておくのも悪くないかも知れない。
 余談だが、この辺が一般的に言える GUI の落とし穴で、作るのは簡単だけれど、後で忘れたころに見直すのに不便である。と言うか、全てを見切るのがホネである。デフォルトの配線に対して変更した配線だけが一望できるようなツールが欲しいところである。「Cocoaはヤッパリ」の中に配線の様子を示した絵があるのだが、ああいう表示モードが本当にあるのだと思って、最初の頃に一生懸命探してしまった記憶がある。
 Objective-C の記法が、キーボードから打ち込むには疲れるが(もちろん普通はマニュアルをコピペするのが常道だが)、後で忘れた頃に見直した時に、実に良く理解できるのとは対称的である。

「Controller部」のコーディング

 「Interface Builder」で、NSObjectのサブクラスにMyControlObjectを作成し、アウトレットを3つ、アクションを1つ加え、コロッとインスタンス化して、コード化もする。ボタンからはアクションへ、3つのアウトレットからは四角やボタンへ配線を張る。このようにボタンとかに配線を張る必要があるけれどウインドウに張り付いていない自作プログラムは、ここでコロッとインスタンス化することが必須となる。
 MyControlObject.hファイル。ここでは自動生成されたまま、全く手を加える必要はない。


/* MyControlObject */

#import <Cocoa/Cocoa.h>

@interface MyControlObject : NSObject
{
    IBOutlet id myOutlet1;
    IBOutlet id myOutlet2;
    IBOutlet id myOutlet3;
}
- (IBAction)myAction:(id)sender;
@end
    

 書き直し後のMyControlObject.mファイル。MyModelObjectを呼びたいので、MyModelObject.h のインポートを追加している。


#import "MyControlObject.h"
#import "MyModelObject.h"

@implementation MyControlObject

BOOL model=NO;

- (IBAction)myAction:(id)sender
{
    if( model==NO ){
        [ MyModelObject initWith: [ myOutlet1 doubleValue ] ];	// modelの初期化
        [ myOutlet2 setTitle: @"ADD" ];				// ボタンのタイトルをADDに変更
        model = YES;
    }else
        [ MyModelObject addData: [myOutlet1 doubleValue] ];	// modelへのデータの追加

    [ myOutlet3 setDoubleValue: [ MyModelObject lookData ] ];	// modelからのデータ読み出し
}

@end
    

 Modelの中身は全てクラスメソッドなので、インスタンス化する必要は無く、呼び出しは[クラス名 メソッド名]で行なう。言ってしまえば通常の関数呼び出しと同じと言うことである。

サンプルソフトのダウンロード

 あえてダウンロードするまでも無いソフトですが、UPしておきます。 たったの12KB! ソースのみですので「Project Builder」でコンパイルして下さい。

ちゃんとインスタンス化してみる(その1)

 いくらなんでも、クラスメソッドでの実装は邪道ですか。失礼しました。しかし、このページを書いた当初の何も良く解っていない私にとっては、インスタンスじゃなくてもちゃんと動くのが解って、とてもほっとした記憶がある。
 でも、あれからまだ数カ月だが、このページの見直し推敲をやっていて、もはや私は「クラスメソッドでの実装は気持ち悪い」と思うようになって来ている。インスタンスを使うことで、いかに世界が広がるかと言うことについて、実感させられているからでしょう。

「Model部」のコーディング

 下に示すのが、ちゃんとインスタンス化した場合の MyModelObject.h ファイル。


#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
    double sumData;
}
+ (id)modelWith:(double)sender;
- (void)addData:(double)sender;
- (double)lookData;
@end
    

 次が MyModelObject.m ファイル。クラスのまま実行した上の例の改良版と言う形で書いてみた。sumData と言う変数が、.h 側に引っ越したことが大きな違いである。


#import "MyModelObject.h"

@implementation MyModelObject

- (id)initWith:(double)sender
{
    sumData = sender;		// 初期値のセット
    return self;
}

+ (id)modelWith:(double)sender
{
    id x = [ [ MyModelObject alloc ] initWith: sender ];
    return x;
}

- (void)addData:(double)sender
{
    sumData += sender;
}

- (double)lookData
{
    return sumData;
}

@end
    

「View部」の作成

 「Moedl部」をインスタンス化したかどうかには影響されないので、変更無し。

「Controller部」のコーディング

 MyControlObject.hファイル。model オブジェクト(前の例ではフラグだったが)の宣言が引っ越してきたのにつれて、MyModelObject.h のインポート宣言もこちらに引っ越した。


/* MyControlObject */

#import <Cocoa/Cocoa.h>
#import "MyModelObject.h"

@interface MyControlObject : NSObject
{
    IBOutlet id myOutlet1;
    IBOutlet id myOutlet2;
    IBOutlet id myOutlet3;
    MyModelObject *model;
}
- (IBAction)myAction:(id)sender;
@end
    

 MyControlObject.mファイル。インスタンス化したので、クラス名でなくオブジェクト名でコールされていることが、大きな違い。


#import "MyControlObject.h"

@implementation MyControlObject

- (IBAction)myAction:(id)sender
{
    if( model==nil ){
        model = [ MyModelObject modelWith: [ myOutlet1 doubleValue ] ];	// modelのインスタンス化
        [ myOutlet2 setTitle: @"ADD" ];					// ボタンのタイトルをADDに変更
    }else
        [ model addData: [myOutlet1 doubleValue] ];			// modelへのデータの追加

    [ myOutlet3 setDoubleValue: [ model lookData ] ];			// modelからのデータ読み出し
}

@end
    

 さて、Model部をインスタンス化せずにクラスメソッドのままで動作させたバージョンに対して、ちゃんとインスタンス化したバージョンを見比べたとき、変数sumDataとmodelが、なぜ.mファイルから.hファイルに引っ越したのか、説明してみる。.mファイルに定義した変数はクラスの変数であり、いくつインスタンスが生成されてもみんなで見ている一つの変数である。これに対して.hファイルに定義した変数はインスタンス変数であり、各インスタンスそれぞれが独自に持っている変数である。 ..と言うことである。後で試しに.mファイルに定義してみて、どのようなバグになるか実感してみると、とても良く理解できると思う。
 もっと正確に言うと、.mファイルに定義した変数はクラスの変数と言うより、C言語の単なる大域変数で、クラス外からも参照可能なものである。また、単純に「.mファイルから.hファイルに引っ越す... 」などと書いてしまったが、これも正確に言うならば、「@implementation 〜 @end」部から「@interface 〜 @endの{ }内」部へ引っ越すと言うべきである。

 もう一つ細かいことだが、なぜ人に見せる必要のないインスタンス変数の宣言までも、.h 側に書かせるのか? Objective C の考案者の意図がつかめず、この件がずっと頭に引っ掛かっている。ずっと後に気付いたことだが、サブクラスが作られたときにインスタンス変数が継承されることを念頭に入れると、.h 側に書く意義があるのだと言うのも、一つの理由のようだ。しかしそれなら、サブクラスにさえも見せる気のないインスタンス変数であれば、.m 側だけに定義しても良いことになる。この話の続きは難しくなるのでまた別の機会にしましょう。

サンプルソフトのダウンロード

 Model部をちゃんとインスタンス化するバージョンも、UPしておきます。 これもたったの12KB! 参考にして下さい。

ちゃんとインスタンス化してみる(その2)

 上の例ではカッコをつけて、わざわざインスタンス化を実行するクラスメソッドを実装してみたが、だんだん慣れてくると、敢えてそんなことをしなくても良いことが解ってくる。インスタンス化されるクラスの側にインスタンス化を実行するメソッドを置くのと、使用する側がインスタンス化するのとどう違うのか? デフォルトのクラス達にも両方あることは知っての通りである。これは、単なる使い勝手の違いではなくて、インスタンスが破棄されるタイミングについて理解してからでないと、説明できない。詳しいことは「お行儀良く味見」に書くとして、実際問題この例題ではどちらでもちゃんと動作する。

「Model部」のコーディング

 MyModelObject.h ファイル。


/* MyModelObject */

#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
    double sumData;
}
- (id)initWith:(double)sender;
- (void)addData:(double)sender;
- (double)lookData;
@end
    

  MyModelObject.m ファイル。


#import "MyModelObject.h"

@implementation MyModelObject

- (id)initWith:(double)sender
{
    sumData = sender;		// 初期値のセット
    return self;
}

- (void)addData:(double)sender
{
    sumData += sender;
}

- (double)lookData
{
    return sumData;
}

@end
    

「Controller部」のコーディング

 MyControlObject.hファイル。(その1)と同じ。


/* MyControlObject */

#import <Cocoa/Cocoa.h>
#import "MyModelObject.h"

@interface MyControlObject : NSObject
{
    IBOutlet id myOutlet1;
    IBOutlet id myOutlet2;
    IBOutlet id myOutlet3;
    MyModelObject *model;
}
- (IBAction)myAction:(id)sender;
@end
    

 MyControlObject.mファイル。


#import "MyControlObject.h"

@implementation MyControlObject

- (IBAction)myAction:(id)sender
{
    if( model==nil ){
        model = [ [ MyModelObject alloc ] initWith: [ myOutlet1 doubleValue ] ]; // modelの初期化
        [ myOutlet2 setTitle: @"ADD" ];			// ボタンのタイトルをADDに変更
    }else
        [ model addData: [myOutlet1 doubleValue] ];	// modelへのデータの追加

    [ myOutlet3 setDoubleValue: [ model lookData ] ];	// modelからのデータ読み出し
}

@end
    

サンプルソフトのダウンロード

 これも、UPしておきます。 これもたったの12KB! 参考にして下さい。

インスタンスの動作確認

 さて、上でModel部もちゃんとインスタンス化したからには、この機能が複数のインスタンスとなっても、それぞれがちゃんと独立して別々に動作するものかどうか、ぜひとも確かめて見たくなる。
 いろいろな確かめ方があるとは思うが、ここでは「Interface Builder」だけを使ってちょいちょいっと確認してみる。まずウィンドウ上の二つの四角やボタンをひっくるめて選択し、コピーペーストでもう一組作って配置する。当然、コピーされた新しいものには配線の定義が白紙になっている。MyControlObject に関しても、クラスをもう1回コロッとインスタンス化するか、または既にあるインスタンスの複製をつくる。新しい二つの四角やボタンと新しい MyControlObject インスタンスに関しても、最初のものと同様に配線を張れば、これで完了。
 さて、コンパイルして実行して見ましょう。

 左半分と右半分がそれぞれちゃんと独立して別々に動作するところが面白いです。コードには何の手も加えず、「Interface Builder」の GUI 操作だけでこれを達成できたことで、さすがCocoaは凄いぞと感心させられます。

サンプルソフトのダウンロード

 自分で書き直して実感しないとつまらないとは思いますが、完成品をUPしておきます。 やっぱり12KB! ソースのみですので「Project Builder」でコンパイルして下さい。

ファイル分割の必要性

 さて話は変わって「Interface Builder」にコード化をお願いすると、各クラス毎に.hファイルと.mファイルを作ってくれる。このまま利用して何の不都合もないのだが、はたして.hファイルは必須なのか?と言う素朴な疑問が、古風なC言語ユーザーとしては気になってしかたがない。このあとの話はこう言うプログラミング法が良いのだと言うつもりで紹介するのでは無く、こう言う実験的ソフトを通して、Cocoaの理解を深めているだけであることを理解して読んで下さい。
 試しに.hファイルを消去して、.mファイルの「#import ファイル名」と書いてある行の代わりに.hファイルの中身を具体的に書き込んでみる。

  MyModelObject.m ファイル。


#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
    double sumData;
}
- (id)initWith:(double)sender;
- (void)addData:(double)sender;
- (double)lookData;
@end

@implementation MyModelObject

- (id)initWith:(double)sender
{
    sumData = sender;		// 初期値のセット
    return self;
}

- (void)addData:(double)sender
{
    sumData += sender;
}

- (double)lookData
{
    return sumData;
}

@end
    

 MyControlObject.mファイル。


#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
    double sumData;
}
- (id)initWith:(double)sender;
- (void)addData:(double)sender;
- (double)lookData;
@end

@interface MyControlObject : NSObject
{
    IBOutlet id myOutlet1;
    IBOutlet id myOutlet2;
    IBOutlet id myOutlet3;
    MyModelObject *model;
}
- (IBAction)myAction:(id)sender;
@end

@implementation MyControlObject

- (IBAction)myAction:(id)sender
{
    if( model==nil ){
        model = [ [ MyModelObject alloc ] initWith: [ myOutlet1 doubleValue ] ]; // modelの初期化
        [ myOutlet2 setTitle: @"ADD" ];			// ボタンのタイトルをADDに変更
    }else
        [ model addData: [myOutlet1 doubleValue] ];	// modelへのデータの追加

    [ myOutlet3 setDoubleValue: [ model lookData ] ];	// modelからのデータ読み出し
}

@end
    

 やってみると、.hファイル無しで見事にコンパイル実行ができることが解る。

 それでは、ついでなので、徹底的にやってみよう。現在.mファイルは3個あるわけだが、これが1個ならどうだろうか。全部を main.m ファイルの中に書き込んで、ファイルを一つにして見た。

  main.m ファイル。


#import <Cocoa/Cocoa.h>

@interface MyModelObject : NSObject
{
    double sumData;
}
- (id)initWith:(double)sender;
- (void)addData:(double)sender;
- (double)lookData;
@end

@implementation MyModelObject

- (id)initWith:(double)sender
{
    sumData = sender;		// 初期値のセット
    return self;
}

- (void)addData:(double)sender
{
    sumData += sender;
}

- (double)lookData
{
    return sumData;
}

@end

@interface MyControlObject : NSObject
{
    IBOutlet id myOutlet1;
    IBOutlet id myOutlet2;
    IBOutlet id myOutlet3;
    MyModelObject *model;
}
- (IBAction)myAction:(id)sender;
@end

@implementation MyControlObject

- (IBAction)myAction:(id)sender
{
    if( model==nil ){
        model = [ [ MyModelObject alloc ] initWith: [ myOutlet1 doubleValue ] ]; // modelの初期化
        [ myOutlet2 setTitle: @"ADD" ];			// ボタンのタイトルをADDに変更
    }else
        [ model addData: [myOutlet1 doubleValue] ];	// modelへのデータの追加

    [ myOutlet3 setDoubleValue: [ model lookData ] ];	// modelからのデータ読み出し
}

@end

int main(int argc, const char *argv[])
{
    return NSApplicationMain(argc, argv);
}
    

 この実験の意義は、「オブジェクトの名前はファイル名とは関係ない」と言うことの確認である。当たり前じゃないか!と偉そうに言う人もいるだろうが、Cocoaの「Project Builder」で初めてプログラムの世界に入ってきた人にとっては、オブジェクトの名前が常にファイル名と同じと言うサンプルしか見たことがない訳で、こんなことが気になって無用の勘違いをすることもあるかも知れない。
 もちろん実際には、.h ファイルがオブジェクトごとに別ファイルになっていなかったり、.h ファイル名がオブジェクト名と違っていたら、インクルードするときに不便だし、実用性がないことは言うまでもない。

2002.5.11
最初の記事アップは、2002.1.20 でしたが、 後で読み返してみると気になる文章が多かったので、2002.4.19 に推敲を加え、今回 2002.5.11 に大改修を加えました。参考までに最初のつたないページもここに残しておきます。


次の話題、3. ちょっとしたカレンダーが欲しい