前回までは主に、Pythonのオブジェクト指向の文法の話をしました。今回は特に新しい文法について扱いません。それよりも「オブジェクト指向をどう使うのか」ということに着目したいと思います。
オブジェクトって何?
いきなりそもそも論です。オブジェクト指向の「オブジェクト」って具体的には何なんでしょうか。
結論から言ってしまうと、オブジェクトとインスタンスは非常に近い存在です。ただ、インスタンスは「クラスから生成される」という意味合いが強く、これは以前お話した、たい焼きの型枠とたい焼きといったような文脈で使われます。それに対して、オブジェクトは「ある特定の領域をカバーする実体」という意味合いが強いかもしれません。また、クラスとクラスの連携を意識する際も「オブジェクト」という言葉が良く使われます。
「ある特定の領域をカバーする実体」や「連携」と言われても少しわかりにくいと思うので例をあげて説明しましょう。以下の図を見てください。
これは簡単なWebサービスを作る際に必要な人物と、それぞれの仕事および関係を示しています(開発論についての話題ではないので、もっとこうしたほうがよいというようなことは置いておいてください)。
上図を見てわかるようにそれぞれの登場人物が果たすべき仕事は明確に分けられており、各人物の間の関係も明確です。この図におけるそれぞれの登場人物が、オブジェクト指向のオブジェクトにあたります。
つまり、オブジェクト指向におけるオブジェクトは単にクラスから生成された実体(インスタンス)であるという意味合いだけでなく、以下の特徴があります。
- それぞれが果たす役割が明確に決まっている
- オブジェクト同士を連携させて全体像を作る
この「役割が明確」「連携させて全体となる」といったことは手続き型言語でも実現可能であり、実際に優秀なエンジニアはこれらを意識して設計を行います。ただ、オブジェクト指向言語は、設計さえ正しければこれらを容易に実現できるのに対して、手続き型言語は各プログラマの技量に強く依存し、なおかつ全体で情報の共有をより多く必要とします。要は、手続き型言語では開発のリーダーや各プログラマの腕が悪いと簡単にアーキテクチャが崩壊するということです。
オブジェクトはオブジェクトを持つ(コンポジション)
オブジェクトとオブジェクトは連携するということがわかっていただけたかと思います。ただ、問題はこれをどう実現するかです。
実は、これはすごい単純な話で、オブジェクトがオブジェクトを持てば解決します。たとえば自動車という複雑な製品をイメージしてください。車は数万以上のパーツから構成されているものの、それらのパーツはある単位にまとめられていますね。たとえば、構成要素として、エンジン、4つのタイヤ、ハンドルもあげられます。
車というオブジェクトが金属パーツ数千個、ネジを数千個持つと考えるのではなく、車というオブジェクトが、エンジンオブジェクト、4つのタイヤオブジェクト、ハンドルオブジェクトを持つと考えるのです。こうすることで、車というオブジェクトを人が理解しやすくなり、設計も簡単になります。
このように、オブジェクトがオブジェクトを持つということを「コンポジション」と呼びます。コンポジションは当然ながら一階層ではなく、複数の階層になる場合があります。たとえば、車オブジェクトはタイヤオブジェクトを持ち、タイヤオブジェクトはブレーキオブジェクトを持つという構成も考えられますね。まぁ、車にそれほど詳しくないので間違っているかもしれませんが(笑)。
コンポジションの実現
さて、話が長くなってしまいましたが、実際にどのようにしてコンポジションを実現するかについて話します。あえて話すことでもないほど簡単ですが。
先ほどのWebサービスの開発の仕組みを簡単にして、以下の図のようなものを構築してみたいと思います。
見てわかるようにManagerオブジェクトのBobが、EngineerオブジェクトのTomに作業を依頼するというものです。実際にはありえない話でしょうが、上司のBobは算数ができないため、Tomに計算処理を依頼するというシナリオでコードを書いてみます。
class Manager:
def __init__(self):
self.tom = Engineer()
def work_a(self):
result = self.tom.add(5, 3)
print(result)
def work_b(self):
result = self.tom.add(8, 4)
print(result)
class Engineer:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
bob = Manager()
bob.work_a()
# 8
bob.work_b()
# 12
注目して欲しいところは2点あります。ひとつはManagerクラスがEngineerのインスタンスであるtomを持っているという点。Managerのメソッドを見ると、tomに仕事を依頼して、その結果を得ていることがわかりますね。もうひとつは、Managerクラスは自分が定義されている箇所より後半にあるEngineerクラスを利用できているということです。たとえば、
a = 5
print(a + b)
b = 5
というプログラムは2行目でエラーとなりますが、先のプログラムでは、3行目の'self.tom = Engineer()'でエラーとなりません。これはこの行をPythonが読み込んだ際にはまだ実行されておらず、なおかつ文法的に間違いがないためです。言ってしまえば'self.tom = Engineer2()'という存在しないクラスを参照していてもエラーにならず、実行時にエラーとなります。
どうです、クラスが別のクラスを持ち、それを利用するというのはそれほど難しくないですよね。もうちょっとプログラムを複雑にしてオブジェクト指向のメリットを得てみたいと思います。
この無能なマネージャーBobは仕事が管理できておらず、どのようなことを会議で話していたかまったく覚えられないとしましょう。そこで優秀な秘書Saraにログを取ってもらうことにしました。
これをプログラムで実現してみます。この秘書は算数をするエンジニアより優秀で複雑なので、今回はモジュールに分けてコードを整理します。
まず、秘書のSecretaryクラスです。secretary.pyに書かれています。
import time
class Secretary:
def __init__(self):
self.log = []
def write_log(self, text):
self.log.append(time.ctime() + ': ' + text)
def get_log(self):
return '\n'.join(self.log)
コードを見てもらうとわかりますが、listにログを時間付きで保存していき、ログを取得する際はそれを改行コードで結合してstringとして返しています。
次に上司のManagerクラスを定義し、先ほどのEngineerと同じようにSecretaryクラスを利用してみます。
import time
from secretary import *
class Manager:
def __init__(self):
self.sara = Secretary()
def work_a(self):
self.sara.write_log('hello')
time.sleep(5)
self.sara.write_log('hey')
def work_b(self):
print(self.sara.get_log())
bob = Manager()
bob.work_a()
bob.work_b()
# Wed Oct 14 21:33:01 2015: hello
# Wed Oct 14 21:33:06 2015: hey
実行すると、上司の発言ログが時間付きで得られていますね。それほど難しくないと思います。
クラス分けによる実装のシンプル化
さて、先に扱ったEngineerとSecretaryの例なのですが、非常に簡単なものの実はかなり一般的な使い方となります。
まずEngineerの例は、特定の複雑な処理をAPIのようにして提供していることがわかります。ただ、処理の主導権自体はManagerにあり、Engineerは100%、Managerが思うままに動きます。正直なところ、このような使い方の場合はモジュールとして独立させて、必要な処理を単なる関数として定義したほうがよいかもしれませんね。そもそもインスタンスを作るメリットがあまりありません。
次にSecretaryですが、これは非常にオブジェクト指向の思想にあった良い使い方です。先ほどのEngineerの例と違って、こちらは内部に「今までのログ」という「状態」を持っていることがわかります。本来はManagerクラスが何を何時に言ったか覚えていなければいけないものを、その作業をすべてSecretaryに肩代わりしてもらっています。Secretaryが状態を持つということは、100%マネージャーの思いどおりになるとは限らないということですが、少なくともあれもこれもとすべてマネージャーにやらせるよりは、専任の人(クラス)にそれをやらせたほうが間違いは少ないですよね。
つまりクラスを使うことで、特定の複雑な処理を簡単に呼び出すようにしたり、ある特定の処理の複雑な状態管理をシンプルに管理できるようになるといえます。クラスを使わない場合と使う場合の比較図を以下に記します。
クラス分けによるコードの再利用
クラス分けをすることにより実装がシンプルになるという話をしましたが、これはコードを再利用する際にも重要なことです。
先ほどのSecretaryクラスのSaraですが、ManagerのBobの上司であるDirectorのJohnも「俺もログ機能を使いたい」と思ったとします。仮にログの機能をSecretaryに実装せずに、直接Managerクラスに実装してしまったとしたら、DirectorにManagerのログ機能を「コピー&ペースト」で実装することになると思います。
一方、きちんと Secretaryクラスとして機能が分けられていれば、DirectorクラスもManagerクラスと同様に、Secretaryクラスを内部に持つことで、ログ機能を簡単に使えるようになります。コピペに比べると随分とエレガントですし、仮にログ機能にバグが見つかったとしても修正は一箇所で済みます。
個人的な考えですが、一般的に自分が設計するクラスは2種類あると考えています。
- アプリやサービスのアーキテクチャを作るクラス
- 上記クラスが利用する汎用処理のクラス
はっきり言うと、すべて汎用クラスとすることはライブラリの開発以外は不可能です。独自の機能は一箇所でしか使われず、それはあなたが開発するアプリやサービスを 特徴付けるものです。ただ、すべてこのような独自機能のクラスに実装を定義してしまうとコードが複雑になり、メンテナンスも大変になります。
そこで独自機能を実現するために「どのような汎用処理が必要か」をよく考えて、それを切り出してクラスとして定義してあげる。自分の独自機能は可能な限り、標準ライブラリや自分が作った汎用クラスを使いながらシンプルなコードで実現する。そうすることで独自性を持たせつつ、なおかつ理解しやすいコードが作れるのではないかと思っています。
詳細な実装の隠蔽
今まで何度も話してきた実装の隠蔽ですが、クラスを使うとこのメリットがわかりやすいので再度取り扱います。
先ほどのクラスSecretaryですが、ひとつ問題があります。それはプログラムを終了するとデータをすべて失ってしまうことです。この問題を解決したいとしましょう。
仮にこの機能をSecretaryクラスではなく、プログラムの主役であるManagerクラスに実装していたとすると、コードの修正はメインとなるクラスをいじらなければならなくなります。メインとなるクラスは一般的に複雑で、なおかついろいろなものを利用しているため、テストなどもしにくいものです。正直なところ、ログの方式の変更程度でメインとなるクラスをいじるのは面倒くさく、なおかつバグを生むというリスクも高まります。
ただ、きちんとSecretaryクラスとしてログ機能が実装されていると、メインとなるManagerクラスをいじらずに、Secretaryクラスのみを変更するだけで機能をアップデートすることができます。
試しにプログラム終了時にデータを失うという問題点を回避する修正を加えてみます。これには以前お話したPickleという機能を使ってみます。
import time, os.path, pickle
class Secretary:
def __init__(self):
self.logfile = '_log.dump'
if(os.path.exists(self.logfile)):
f = open(self.logfile, 'r')
self.log = pickle.load(f)
f.close()
else:
self.log = []
def write_log(self, text):
self.log.append(time.ctime() + ': ' + text)
f = open(self.logfile, 'w')
pickle.dump(self.log, f)
f.close()
def get_log(self):
return '\n'.join(self.log)
コードを読んでもらうとわかりますが、コンストラクタで以前のデータを格納したダンプファイルがあるか確認し、あればそれを読み込み、なければ新しくデータを作成しています。そしてwrite_logで変更をするたびにダンプファイルを書き出しています。
これを利用するManagerクラスを以下に記載します。
import time
from secretary import *
class Manager:
def __init__(self):
self.sara = Secretary()
def work_a(self):
self.sara.write_log('hello')
time.sleep(5)
self.sara.write_log('hey')
def work_b(self):
print(self.sara.get_log())
bob = Manager()
bob.work_a()
bob.work_b()
先に書いたManagerクラスと見比べてみてください。一行も変わっていないですね。これは手抜きではなく、あえてそうしています(笑)。プログラムのメインとなるManagerのコードを一切変更せずに、ログ機能のアップデートを行っています。
試しにコードを動かしてみます。一度目の実行は、
Thu Oct 15 07:33:43 2015: hello
Thu Oct 15 07:33:48 2015: hey
となりました。そして二度目の実行は、
Thu Oct 15 07:33:43 2015: hello
Thu Oct 15 07:33:48 2015: hey
Thu Oct 15 07:34:18 2015: hello
Thu Oct 15 07:34:23 2015: hey
となっています。ログを残すという目的を果たせていることがわかりますね。このように適切に機能がクラス分けされていると、コードの修正範囲が狭くなり、機能拡張や変更、そしてバグ潰しが楽になります。大切なことなので覚えておいてくださいね。
情報の隠蔽によるエキスパートの協業
情報を隠蔽するメリットは、何も修正や改良だけではありません。話を車の開発に戻しましょう。
車が登場しはじめた当時は、おそらく車のすべてのパーツに対して開発者が熟知している必要があったはずです。ただ、そのようなことはどんどん複雑になってきている現在の車ではできないはずです。各自動車メーカーにはエンジンの専門家やフレーム開発の専門家、ブレーキの専門家といったようにある特定のパーツに特化した専門家を多数そろえていて、それぞれの専門家は、自分の範囲外の分野について、深い知識はそれほど必要としません。たとえばブレーキの専門家は、エンジンの省エネ化方式については、知る必要はないですよね。一方、車の全体設計をするエンジニアは、各パーツを組み合わせて車を作ったり、新しい車を作るために必要な要件を専門家に伝えればよいだけです。
オブジェクト指向もこれと同じことが言えます。各オブジェクトを作っている人たちは「自分が作るオブジェクト(クラス)」については深く理解する必要があるものの、「自分が利用するオブジェクト」についてはどう使えばよいかだけ知っておけばよいのです。
ある複雑なモノやシステムも、それを分解していけば小さな単位に分けることができるはずです。複雑なものをどのように自然な実現しやすいコンポーネント単位にまとめるか。プログラマのスキルは「細かい処理をいかにして実現するか」ということだけではなく、「全体像を描き、いかに最適な論理的な構造を作るか」という場面でも必要とされます。
正しい設計がシステムやサービスの構築には必須です。たくさんコードを書いて、設計に失敗して、修正して、そういったことを繰り返していると自然と正しい設計ができるようになるのではないでしょうか。デザインパターンという魔道書を読むのもてっとり早いのですが、ある程度の経験がないと自分の血肉にはならないです。
どうやってオブジェクトをもたせるか
さて、少々初級レベルを逸脱しはじめてきたのですが、そろそろ今回の最後のテーマに移りたいと思います。それは「インスタンスにどうやってオブジェクトを持たせるか」という内容です。
今までのManager、Engineer、Secretaryの例を見てもらうとわかるのですが、常に「AがBを持つ」という形でプログラムが実現されていますね。そのため、Manager内でEngineerやSecretaryのインスタンスを作成して保持すればよいだけでした。
ただ、世の中(プログラム)はそんなに単純じゃありません。たとえば以下のシナリオを考えてください。
秘書SecretaryクラスのSaraは昇進したので、ログ取りという業務ではなく、客からのアポイントメントの管理をするという仕事を新たにすることになりました。この業務は以下の図のようになります。
ManagerクラスのBobがSaraにアポイントを確認するのは簡単です。今までどおり、自分が持つSaraオブジェクトのメソッドを呼び出すだけです。ただ、問題となるのは「客がSaraにどうやってアポイントメントを取るか」です。なんせ、SaraはBobが持っており、客はSaraを持っていないのですから。
これを実現する方法はさまざまですが、共通して言えることは「Bobが持つSaraというインスタンスを客に渡す必要がある」ということです。自分で作るのではなく、渡すことでSaraというインスタンスをBobとクライアントのインスタンスが共有します。
これをコードで実現してみます。まず最初に、秘書のSecretaryクラスです。時間管理は今回の本質ではないので、辞書型を使って "時間:誰" という形でアポイントを管理させています。同じ時間を指定されたときだけFalse(アポイントを取れない)を返して、そうでなければTrue(アポイントを取れた)を返します。
class Secretary:
def __init__(self):
self.appointment = {}
def request_appointment(self, when, who):
if(when in self.appointment):
return False
else:
self.appointment[when] = who
return True
def get_schedule(self):
return str(self.appointment)
次にManagerとClient、そして実行コードです。Managerはアポイントの確認機能、クライアントはアポイントを取る機能が実装されています。
from secretary import *
class Manager:
def __init__(self):
self.sara = Secretary()
def check_schedule(self):
schedule = self.sara.get_schedule()
print(schedule)
def get_secretary(self):
return self.sara
class Client:
def __init__(self, name):
self.name = name
self.contact_point = None
def set_contact_point(self, contact_point):
self.contact_point = contact_point
def make_appointment(self, when):
if(self.contact_point):
is_success = self.contact_point.request_appointment(when, self.name)
print(self.name + " could book? : " + str(is_success))
bob = Manager()
adam = Client('adam')
adam.set_contact_point(bob.get_secretary())
adam.make_appointment('10:30')
charles = Client('charles')
charles.set_contact_point(bob.get_secretary())
charles.make_appointment('11:30')
dag = Client('dag')
dag.set_contact_point(bob.get_secretary())
dag.make_appointment('10:30')
bob.check_schedule()
少し長いですが、実行させると以下のようにきちんと動いていることがわかります。
adam could book? : True
charles could book? : True
dag could book? : False
{'11:30': 'charles', '10:30': 'adam'}
注目してほしいことは、Clientのインスタンスが、Managerの持つSecretaryのインスタンスを取得していることです。このようにすることで、特定のインスタンスを複数の異なるインスタンス間で共有することができ、その共有されたインスタンスを介してやりとりすることが可能です。
今回はManagerにget_secretary、Clientにset_contact_pointというメソッドを実装してやりとりしています。ただ、インスタンスの受け渡しの方法はこれだけではなく、ぱっと思いつくだけでも以下の方法があります。
- コンストラクタでインスタンスを渡す
- get、setで渡す(今回の例)
- インスタンスの受け渡し専門のクラスを作る
- グローバル空間で共有する
やりかたはさまざまですが、それぞれにメリット・デメリットがあるので状況に応じて使い分ける必要があります。まぁ、これに関しては一概的な手法はないので、いろいろやってみてください。
今回でオブジェクト指向の前半戦は終了です。次回は今までの復習も兼ねて簡単なゲーム作りをしてみたいと思います。