ifと別れる50の方法:Null Object
先の実現方法では、左右の部分木を持たないことを判定するのに、if文が必要でした。伝統的な構造化プログラミング(SP)では重宝されるif文も、オブジェクト指向プログラミング(OOP)では逆にそれが手枷足枷となります。if文を卒業すると、また一歩OOPの神髄へと近づけます。if文の呪縛から逃れるにはいくつかの術がありますが、ここでは、Null Objectを紹介します。Null Objectに関する広範な論議は、Smalltalkを参考にするのが良いでしょう。
class BinTree:
tab = " "*6
クラスBinTreeでは、抽象クラスとして子孫クラスに共通の特性を規定します。ここでは、インスタンス変数tabがタブに相当する文字列を保持します。
class TNone(BinTree):
def __str__(self): return ""
def __len__(self): return 0
def max_len_word(self): return 0
def table(self): return ""
def _table(self, format): return ""
def tree(self): return ""
def _tree(self, indent): return "%s+--\n"%(self.tab*indent)
クラスTNoneは、左右の部分木を持たない「末端にある」二分木のノード(葉)を表わします。左右の部分木を持たないので、どのメソッドも単純に実現できます。
class Tnode(BinTree):
none = TNone()
def __init__(self, word="", count=0, left=none, right=none):
self.word = word
self.count = count
self.left = left
self.right = right
def __str__(self):
return "%s(%d,%s,%s)"%(
self.word, self.count,
(self.left , "")[not self.left ],
(self.right, "")[not self.right],
)
def __len__(self):
node = 1
left = len(self.left)
right = len(self.right)
return left + node + right
def max_len_word(self):
node = len(self.word)
left = self.left .max_len_word()
right = self.right.max_len_word()
return max(left, node, right)
def table(self):
format = "%%%dr: %%d\n"%(self.max_len_word()+2)
return self._table(format)
def _table(self, format):
node = format%(self.word, self.count)
left = self.left ._table(format)
right = self.right._table(format)
return left + node + right
def tree(self):
return self._tree(0)
def _tree(self, indent):
node = "%s+-- %r(%d)\n"%(
self.tab*indent, self.word, self.count)
left = self.left ._tree(indent+1)
right = self.right._tree(indent+1)
return left + node + right
クラスTnodeは、左右の部分木を持つ「末端にない」二分木のノード(節)を表わします。インスタンス変数noneは、葉TNone()を保持します。これは、メソッド__init__において、生成したてのTnodeインスタンスを初期設定するときに、左右の部分木の規定値として利用されます。他のすべてのメソッドを実現するときには、左右の部分木を持たないこと(境界条件)を判定していた、すべての冗長なif文を排除できます。そのため、見通しの良い簡潔なコードを記述できます。
TNone/Tnodeを区別せずに、BinTreeですべてを仕切ろうとすると、事態はさらに混沌とします。if文が至る所に散在すると、コードの見通しが悪くなるだけでなく、要求仕様の変更に伴うメンテナンスの悪夢(西暦二千年問題)が蘇ります。単純な二分木の例題でも一苦労なのに、実際のシステム開発で同じ病巣を抱えたいと思いますか。ifと別れる50の方法:Dynamic Class
先と同じく、if文の呪縛から逃れる術として、Dynamic Classを紹介します。Dynamic Classに関する広範な論議も、Smalltalkを参考にするのが良いでしょう。
class Tnode: tab = " "*6
クラスTnodeは、左右の部分木を持つ「末端にない」二分木のノード(節)を表わします。この段階では、まだメソッドを再定義していません。
TNone = Tnode() TNone.__str__ = lambda: "." TNone.__len__ = lambda: 0 TNone.max_len_word = lambda: 0 TNone.table = lambda: "" TNone._table = lambda format: "" TNone.tree = lambda: "" TNone._tree = lambda indent: "%s+--\n"%(Tnode.tab*indent)
変数TNoneは、クラスTnodeのインスタンスの一つで、「末端にある」二分木のノード(葉)を表現します。左右の部分木を持たないので、どのメソッドも単純に実現できます。これらはみな、TNoneインスタンスに固有のメソッドとなります。
class Tnode:
tab = " "*6
def __init__(self, word="", count=0, left=TNone, right=TNone):
self.word = word
self.count = count
self.left = left
self.right = right
...
クラスTnodeは、TNoneインスタンスを生成した後でメソッドを再定義します。TNoneインスタンスは葉を保持します。これは、メソッド__init__において、生成したてのTnodeインスタンスを初期設定するときに、左右の部分木の規定値として利用されます。他のすべてのメソッドを実現するときには、境界条件を判定していた、すべての冗長なif文を排除できるので、見通しの良い簡潔なコードを記述できます。
継承に警鐘を鳴らす
各インスタンスには(共通するクラスで定義されている/いないに関わらず)固有のメソッドを規定できます。クラス/クラス間のメソッド継承では、親クラスで規定したメソッドを再定義/再利用できます。同様に、クラス/インスタンス間のメソッド継承では、クラスで規定したメソッドを再定義/再利用できます。
TNoneは、同じクラスTnodeのインスタンスでありながら、他のノードとは世代が異なります。つまり、世代を越えたインスタンスが、相互に情報を交換しながら、共通する問題の解決に貢献します。すると、Null Objectでは、抽象クラスを含めて複数のクラスのインスタンスが相互作用しますが、Dynamic Classでは、世代の異なる同一のクラスのインスタンスが相互作用します。
TNone(および前項で紹介したTnode.none)は、sole instanceの一例です。GoFのデザインパターンでは、これがSingletonとして登録されています。
OOPへの道:初めの一歩、末の千里
皆さんは今、正統な血を引くOOPの教典を求めて、天竺へと続く道の「初めの一歩」を踏み出したものとします。すると、次のような道標に遭遇するかもしれません。
《症状 1b》これはCで書かれてるからオブジェクト指向でない
セミナー会場やコンサルティング現場では、Javaで書かれていても、オブジェクト指向とは呼べない代物に遭遇します。逆に、Cで書かれていながら、OOPの精神を受け継ぐ秀作もあります。C言語でもOOPは可能です。論より証拠。初期のC++は、C言語に変換されてから、コンパイルされていました。
最初の分岐点を過ぎると、今度は、次のような道標に遭遇するかもしれません。
クラスはただの「型紙」ではありません。確かに、クラスには型紙としての特徴があります。しかし、それはクラスが持つ特徴の一つに過ぎず、他のインスタンスと同格に「一人前のオブジェクト first-class object」としても扱われます。インスタンスであれば(オブジェクトの特徴を活かして)実行時にその状態(属性)を変化させます(そうでないと存在意義がありません)。クラスも同様に(オブジェクトの特徴を活かして)実行時にその状態(操作)を変化させます(だからこそ存在意義があります)。クラスは、実行時に成長を続けるので、動的な問題解決に適した(型紙にはない)特徴を発揮できます。これらは、agile言語には欠かせない特徴の一つです。
未熟な言語仕様のツケを清算するのはアナタ?
プログラミング言語を設計するときの背景に、クラス/インスタンスの違いを包括する概念モデルがあると「実行時に属性を変更できるなら、その操作も変更できて当然」と考えます。Ruby/Pythonでは、Java/C#に課された不条理な制約がなく、洗練されたOOPの世界を堪能できます。未熟な言語仕様のツケを、プログラマーが清算する必要もありません。
海外に長く滞在して外国語に習熟するにつれて、日常会話で日本語を使う機会も減ってきます。同様に、OOP言語に習熟するにつれて、if/for/配列(goto/switchは論外)を使う機会も減ってくるものです。それは、成長を自覚できる証にもなります。身近に「C言語などの非OOP言語と違って、Javaでは」と語る人がいたら、Javaでプログラムは書けるとしても「OOPを理解できているか」確認しておく必要があるかもしれません。
OOPの奥義を極める旅の途中では、Java/C#の常識が通用しない場面に遭遇することがあります。例えば、クラスをインスタンスとして扱えたり、逆に、インスタンスをクラスに変身させたり。あるクラスのインスタンスが養子縁組をして、他のクラスのインスタンスになったり。もう、何でもありの世界です。幾度か、立ち止まって思案に暮れるのもいいでしょう。幾度も、振り出しに戻ることがあるかもしれません。それでも焦らずに、一歩ずつ着実に前進あるのみです。
洗練されたOOPの支援機能が得られるなら、アジャイル開発はさらに加速します。皆さんも、サイヤ人から超サイヤ人に変身できるのです。天竺へ続く道は、まだ遠く険しいかもしれませんが、ドラゴンボールを一つずつ探す道程を楽しんでください。
この症状は、実際の開発現場でもよく見掛けます。if文の呪縛から逃れたいなら、その術を探す前に、関数とメソッドとの違いを理解しておくことが肝要です。それが、末の千里へと繋がります。今はまだ、初めの一歩を踏み出したばかりです。その話題は、後の連載で紹介します。
