デザインパターンをObjective-C + Cocoaで解釈してみるこの連載も、いよいよ終わりに近づいてきた。残すところ、あと2パターンである。今回は、Template Methodパターンを取り上げる。
Template Methodとは
Template Methodも、前回のStrategyと同じく、アルゴリズムに関するパターンになる。あるアルゴリズムを実現するときに、それをいくつかのステップに分割しておく。そうしておくことで、アルゴリズムを拡張する際には、サブクラスでそのステップを上書きすればいいということになる。これが、Template Methodパターンだ。
このパターンも、オブジェクト指向の考え方そのものと言えるだろう。GoF本でも、「template methodはたいへん基本的なもので、ほとんどすべての抽象クラスで見つける事ができる」と書かれている。オブジェクト指向プログラミングに慣れたプログラマならば、自然にこのパターンを自分のプログラムに取り入れているはずだ。
Template Methodパターンのあり方や実装方法について、ここで議論する事はあまりないだろう。それよりも、Cocoaで使われているパターンを見てみよう。Template Methodは非常に基本的なものなので、Cocoaのあらゆるところで見つける事が出来る。どれを取り上げてもよいのだが、折角なので、GoF本に従ってみる。GoF本では、Template Methodのサンプルコードとして、AppKitに基づくものを取り上げている。これのオリジナルを紹介しよう。NSViewの描画に関するメソッドになる。
NSViewとdrawRect:
Cocoaでは、画面への描画は、NSViewというクラスが担当している。何らかの描画を行いたい場合は、このクラスのサブクラスを作る事になる。
このクラスが提供する描画を実行するためのメソッドはただ1つ、drawRect:である。
List 1.
- (void)drawRect:(NSRect)dirtyRect
NSViewのサブクラスは、このメソッドを上書きして、描画処理を実行する。再描画しなくてはいけない領域は、引数のdirtyRectで示されている。この領域に対してしか処理を行わないことで、処理速度を向上させる事も出来る。
この構造を、Template Methodとして捉えることが出来るだろう。NSViewが、テンプレートとなる抽象クラスである。分割されたアルゴリズムは、drawRect:メソッドとなる。サブクラスはこのメソッドを上書きする事で、アルゴリズムを拡張出来る。確かに、Template Methodの特徴を備えている。
いまや、多くのアプリケーションフレームワークは、このような構造を備えているだろう。画面への描画や、ユーザインタフェースの提供は、GUIを持つアプリケーションのフレームワークにとって必須の機能である。その多くは、描画クラスと描画メソッドを提供し、それのサブクラスを作る事で新しいGUI部品を作っていく。オブジェクト指向やTemplate Methodと、GUIシステムは相性がいい。
描画の高速化
ついでなので、Cocoaの描画処理について、もう少し詳しく紹介しよう。
フレームワークの描画処理に求められるのは、なんといっても高速化だろう。描画処理が低速なフレームワークは、それだけで使い物にならない。もちろん、描画速度の大部分は、ハードウェアの能力で決まる。高速なCPUとGPUがあれば、それで事足りる事もある。だが、ソフトウェアのレベルでも出来る事がある。
Cocoaでは描画処理の高速化を、
- 必要な領域しか描画しない
- 必要なときにしか描画しない
というポリシーで実現しようとしている。
前者の、必要な領域しか描画しない、には、drawRect:メソッドの引数を使う。drawRect:で示される描画領域は、そのビューが占める大きさが渡されるのではなく、画面上に描画する際に最低限必要な大きさになるように努力される。たとえば、ビューの上に他のウインドウが重なっている場合、その下の領域は描画する必要がなくなるはずだ。そのようなことを考慮して、可能な限り小さい領域を指定しようとする。アプリケーション側でこれを考慮して、常にビュー全体を再描画するのではなく、必要なところだけ描画するように実装する事で、高速化を実現するのだ。
後者の、必要なときにしか描画しない、というものは、drawRect:を呼び出す頻度が関わってくる。ウインドウ上にビューを配置すると、一番始めに画面に描画するときに、drawRect:が一回呼び出される。この呼び出しの際に、描画の結果がキャッシュされるのだ。これ以降は、描画領域の変更などが起こらない限り、drawRect:は呼び出されない。たとえば、ウインドウをドラッグしただけだと、キャッシュされた描画結果が使われるのだ。これにより、drawRect:の呼び出し回数を減らして、高速化を行っている。
では、プログラムの都合で、ビューの大きさは変更されないが再描画する必要が発生した場合はどうするか。それには、setNeedsDisplay:メソッドを使う。
List 2.
- (void)setNeedsDisplay:(BOOL)flag
このメソッドは、システムに再描画の必要がある事を通知する。これを使う事で、再描画を発生させる事が出来る。ただ気をつけたいのは、このメソッドを呼んですぐ再描画が発生する訳ではない。正確に言えば、このメソッドを呼び出したコンテキストで、直接drawRect:が呼び出される訳ではない。このメソッドで再描画の設定をした後、イベントループが回り、システム全体の描画処理が発生したときに描画されるのだ。これは、システム全体の効率化と、setNeedsDisplay:が複数回呼ばれたとしてもただ一回の再描画にまとめるために、このような仕組みになっている。
このあたりの高速化のための仕組みを理解しながら、効率的な描画処理を実装したい。