#概要:部品の使い方から、組み合わせ方へ
単にプログラムの断片を利用するだけでなく、それらを組み合わせたプログラムの実装に向けて学習を進めていく。
#現状
あれがやりたい、これがやりたいというちょっとした思いつきについては、豊富なライブラリと解説のおかげで何とか実現できるようになりました。一方で、じゃあそれらを組合わせて一つのプログラムを作ろうかとなると、どうしても全体がごちゃごちゃしてしまい、やがて手がつけられなくなってしまいます。
よって、ここではクラスについて学んだことを整理し、「自分で拡張・メンテンナスできるプログラム」を作ることを目的として諸事項をまとめていきます。まだまだ初学者の枠を抜けられないプログラマーなので、アドバイスは大歓迎です。
#クラスの抽象度
###抽象度は適切に設定する
プログラムにおいて、名前付けは重要です。適当な名前をつけてしまうと理解・認識を阻害してしまいます。よって、全体がどうなっているかをしっかりと把握するためには、抽象度を状況に応じて決定しなければなりません。
###抽象度
「Python クラス 抽象度」など検索をかけると、メタクラスの使い方についての情報が大半を占めています。意外と理解に手こずった部分です。
例として、古典的な西洋ファンタジー風ロールプレイングゲームを作るという状況を想像してみます。村人のJohnという一人のキャラクターを扱うクラスを作る際に、果たしてどういう名前をつけるべきなのでしょうか。
まず前提として、世の中の色々なものは同時に複数のグループに所属しています。もちろんJohnも例外ではありません。
ざっと考えるだけでも、Johnにつけられる名前はいくつもあります。ゲームに存在する全ての物体(地面や木、建物、キャラクターなど)であるObject。生物と無生物のうち、生物であるCreature。普通の人以外に、エルフやドワーフなども含むであろう人型生物Humanoid。その人型生物の中でも....と、延々とたどっていった結果、村人のJohnにたどり着きます。
####高すぎる抽象度
「とりあえず生き物ってことでCreatureにしてみるか」となると、以下のような実装になってくると思います。
class Creature:
def __init__(self, hp, mp, start_position, area):
self.hp = hp
self.mp = mp
self.position = start_position
self.area = area
self.oxygen = 100
def move(self, next_position):
if self.area == "land":
self.walk()
self.position = next_position
elif self.area == "water":
self.swim()
self.position = next_position
elif self.area == "sky":
self.fly()
self.position = next_position
def walk(self):
print("てくてく")
def swim(self):
print("すいー")
def fly(self):
print("びゅーん")
Creatureという括りであるため、少なくとも地上で生活する生き物と、水中で生活する生き物と、空中で生活する生き物は分ける必要があるでしょう。地上で生活する生き物が水中に入ることを考えると酸素量も管理しないといけない気がしてきます。空中で生活する生き物は重量管理が重要そうです。
さらに今回考えているのはファンタジー世界なので、瘴気に汚染された魔界に生きる生き物とか、光の当たらない地中で生きる生き物とか出てくる可能性だってあります。それに伴って、別の移動方法や独自ステータスの設定など、加速度的に実装すべきものが増えていくことが予想できます。
####低すぎる抽象度
「とりあえず後のことはさておき、村人という括りにしておくか」と考えれば、以下のような実装になると思います。
class Villager:
def __init__(self, name, hp, start_position):
self.name = name
self.hp = hp
self.position = start_position
def move(self, next_position):
self.position = next_position
print("てくてく")
def speak(self, line="こんにちは。ここは始まりの村だよ"):
print(f"{self.name}「{line}」")
villagerA = Villager("John", 100, [0,0])
villagerA.speak()
villagerA.speak("どちらからいらしたのかな?")
#実行結果
'''
John「こんにちは。ここは始まりの村だよ」
John「どちらからいらしたのかな?」
'''
さっきとは打って変わって、村人だけを対象としているので、実装すべきものを考えるのが楽です。NPCとして扱うなら、その辺を適当にうろつくようにして、話しかけるたら適当なセリフを話すようにしておけば良いでしょう。
一方で、すっきりと実装できると欲がでてきます。例えば城郭都市を作ってそこにNPCを配置することを考えた時、このVillagerは少しいじれば使いまわせそうです。動いて話すという基本機能は変わりませんから。
後々の拡張性も考えると、やはりVillagerという名前が最適とは言い難そうです。居住域をさらに細分化してみたりとか、その居住域ごとに特性をつけるとか。バリエーションをもたせるために攻撃不可能な子供や、逆に攻撃可能なならず者を作ったりとか、そういう拡張ですね。
####適切な抽象度
では、高すぎず低すぎない適切な抽象度を保った名前はどのようなものでしょうか?仕様次第でいくらでも変わりうるでしょうが、妥当なところとしては以下のようになるでしょう。
class Resident:
def __init__(self, name, hp, start_position):
self.name = name
self.hp = hp
self.position = start_position
def move(self, next_position):
self.position = next_position
def speak(self, line="こんにちは"):
print(f"{self.name}「{line}」")
citizenA = Resident("John", 100, [0,0])
citizenA.speak()
villagerA = Resident("Mary", 100, [0,10])
villagerA.speak("こんにちは。ここはサウスシティですか?")
#実行結果
'''
John「こんにちは」
Mary「こんにちは。ここはサウスシティですか?」
'''
どこかに住んでいる存在ということでResidentという名前をつけてみました。これなら村民も市民も含まれます。なんなら空中都市民を作ってしまっても良いかもしれませんね。
###まとめ
抽象度を高めると実装すべきものが増えていきます。乗り物を例に考えると、CarではなくTransportationとして実装しようとすると、飛行機や船まで含まれることになり、これまた実装量が増えていきます。
一方で、抽象度を低くすると実装するべきものは減りますが、使用できる範囲は当然狭くなります。同じく乗り物を例に考えると、CarではなくBusを実装すれば、考えるべき範囲は狭くなり、実装は楽になるでしょう。一方で同じくお客さんを乗せるTaxiへの応用を考えると、これまた別の手間がかかるようになります。
- 抽象度が高すぎると、実装すべきものが増えていく
- 抽象度が低すぎると、他への使いまわしに手間がかかるようになる
###余談
次回以降は抽象基底クラスや、継承と合成・委譲についてのまとめを行っていく予定です。