概要
この記事は、Pythonのデザインパターンに特化した短期連載の最初の記事です。
創造的デザインパターン
Creational Design Patternsは、その名の通り、クラスやオブジェクトを作成するためのパターンである。
クラスの実装にあまり依存しないように、あるいは、必要なときに複雑な構築をする必要がないように、あるいは、特別なインスタンス化特性を保証するように、クラスの仕様を抽象化する役割を果たす。
デザインパターンは、クラス間の依存関係を低くし、ユーザーがクラスとどのようにやり取りするかを制御するのに非常に便利です。
この記事で取り上げるデザインパターンは以下の通りです。
- ファクトリー
- 抽象ファクトリー
- ビルダー
- プロトタイプ
- シングルトン
- オブジェクトプール
工場
問題点
あなたは、フルタイムで雇用されている人に保険を提供する保険会社のソフトウエアを作っているとします。
あなたは Worker
というクラスを使ってアプリケーションを作りました。
しかし、その顧客はビジネスを拡大することを決定し、異なる手続きや条件ではありますが、失業者にもサービスを提供することになりました。
そこで、失業者用にまったく新しいクラスを作成する必要があります。
そのクラスは、まったく別のコンストラクタを使用することになります。
しかし今度は、一般的にどのコンストラクタを呼び出せばいいのか、ましてやどの引数を渡せばいいのか、わからなくなってしまいます。
コンストラクタを呼び出すたびに if
文で囲み、オブジェクト自体の型をチェックするために高価な処理を行うなど、コード中に醜い条件分岐を作ることができます。
もし初期化中にエラーがあれば、それを検出し、コンストラクタが使用される100の場所すべてでそれを実行するようにコードを編集します。
この方法は、あまり好ましくないし、拡張性もなく、持続不可能であることを、あなたはよく理解していることでしょう。
あるいは、ファクトリーパターンを考えてみるのもいいだろう。
解決策
ファクトリーは、使用するクラスに関する情報をカプセル化し、提供する特定のパラメータに基づいてインスタンス化するために使用されます。
ファクトリーを利用することで、最初に実装を決定したパラメータを変更するだけで、別の実装に置き換えることができる。
このように実装と利用を切り離すことで、全く同じコードベースで、新しい実装を追加し、ファクトリーを通してインスタンス化するだけで、簡単にアプリケーションを拡張することができます。
別のファクトリーをパラメータとして受け取るだけなら、それがどのクラスを生成するのか知る必要すらありません。
ある特定の振る舞いをすることが保証されたクラスを返す、統一されたファクトリーメソッドがあればいいのです。
では、見てみましょう。
手始めに、抽象メソッドを含めることを忘れないでください。
from abc import ABC, abstractmethod
生成されるクラスは、それらを統一的に扱うことができるように、いくつかのメソッドのセットを実装する必要があります。
そのために、以下のインターフェイスを実装します。
class Product(ABC):
@abstractmethod
def calculate_risk(self):
pass
そして、それを Worker
と Unemployed
として継承します。
class Worker(Product):
def __init__(self, name, age, hours):
self.name = name
self.age = age
self.hours = hours
def calculate_risk(self):
# Please imagine a more plausible implementation
return self.age + 100/self.hours
def __str__(self):
return self.name+" ["+str(self.age)+"] - "+str(self.hours)+"h/week"
class Unemployed(Product):
def __init__(self, name, age, able):
self.name = name
self.age = age
self.able = able
def calculate_risk(self):
# Please imagine a more plausible implementation
if self.able:
return self.age+10
else:
return self.age+30
def __str__(self):
if self.able:
return self.name+" ["+str(self.age)+"] - able to work"
else:
return self.name+" ["+str(self.age)+"] - unable to work"
さて、人々が揃ったので、彼らの工場を作りましょう。
class PersonFactory:
def get_person(self, type_of_person):
if type_of_person == "worker":
return Worker("Oliver", 22, 30)
if type_of_person == "unemployed":
return Unemployed("Sophie", 33, False)
ここではわかりやすくするためにパラメータをハードコードしていますが、通常はクラスのインスタンスを作成して処理をさせるだけでしょう。
それでは、ファクトリーをインスタンス化して、何人かの人間を生産させてみましょう。
factory = PersonFactory()
product = factory.get_person("worker")
print(product)
product2 = factory.get_person("unemployed")
print(product2)
Oliver [22] - 30h/week
Sophie [33] - unable to work
抽象ファクトリー
問題点
異なるオブジェクトのファミリーを作成する必要があります。
それぞれは異なるが、ある特徴によって何らかの形でグループ化されている。
例えば、イタリアンレストランとフレンチレストランのメインとデザートを作る必要があるが、一方の料理ともう一方の料理を混ぜることはない。
解決策
考え方は通常のファクトリーパターンと非常に似ていますが、唯一の違いは、すべてのファクトリーがオブジェクトを作成するための複数の別々のメソッドを持ち、ファクトリーの種類によってオブジェクトのファミリーを決定することです。
抽象ファクトリーはオブジェクトのグループ全体をそれぞれのファクトリーと一緒に作成する責任を持ちますが、これらのオブジェクトの具体的な実装には関与しません。
その部分はそれぞれのファクトリーに任されます。
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def cook(self):
pass
class FettuccineAlfredo(Product):
name = "Fettuccine Alfredo"
def cook(self):
print("Italian main course prepared: "+self.name)
class Tiramisu(Product):
name = "Tiramisu"
def cook(self):
print("Italian dessert prepared: "+self.name)
class DuckALOrange(Product):
name = "Duck À L'Orange"
def cook(self):
print("French main course prepared: "+self.name)
class CremeBrulee(Product):
name = "Crème brûlée"
def cook(self):
print("French dessert prepared: "+self.name)
class Factory(ABC):
@abstractmethod
def get_dish(type_of_meal):
pass
class ItalianDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return FettuccineAlfredo()
if type_of_meal == "dessert":
return Tiramisu()
def create_dessert(self):
return Tiramisu()
class FrenchDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return DuckALOrange()
if type_of_meal == "dessert":
return CremeBrulee()
class FactoryProducer:
def get_factory(self, type_of_factory):
if type_of_factory == "italian":
return ItalianDishesFactory
if type_of_factory == "french":
return FrenchDishesFactory
両方のファクトリーを作成して、それぞれの cook()
メソッドをすべてのオブジェクトに対して呼び出すことで、その結果をテストすることができます。
fp = FactoryProducer()
fac = fp.get_factory("italian")
main = fac.get_dish("main")
main.cook()
dessert = fac.get_dish("dessert")
dessert.cook()
fac1 = fp.get_factory("french")
main = fac1.get_dish("main")
main.cook()
dessert = fac1.get_dish("dessert")
dessert.cook()
Italian main course prepared: Fettuccine Alfredo
Italian dessert prepared: Tiramisu
French main course prepared: Duck À L'Orange
French dessert prepared: Crème brûlée
問題点
Y
問題点
Y
解決策
ソリューション
W
解決方法
オブジェクトを構築し、適切なモジュールをロボットに追加する Builder クラスを作成することができます。
複雑なコンストラクタの代わりに、オブジェクトをインスタンス化し、関数を使用して必要なコンポーネントを追加することができます。
各モジュールの構築は、オブジェクトをインスタンス化した後に個別に呼び出します。
それでは、Robot
をデフォルト値で定義してみましょう。
def __init__(self, left_leg, right_leg, left_arm, right_arm,
left_wing, right_wing, tail, blades, cameras,
infrared_module, #...
):
self.left_leg = left_leg
if left_leg == None:
bipedal = False
self.right_leg = right_leg
self.left_arm = left_arm
self.right_arm = right_arm
# ...
コンストラクタでは特定の初期化を省略し、代わりにデフォルト値を使用していることに注意してください。
これは、これらの値を初期化するために Builder クラスを使用するためです。
まず、ビルドのためのインターフェイスを定義する抽象的な Builder を実装します。
class Robot:
def __init__(self):
self.bipedal = False
self.quadripedal = False
self.wheeled = False
self.flying = False
self.traversal = []
self.detection_systems = []
def __str__(self):
string = ""
if self.bipedal:
string += "BIPEDAL "
if self.quadripedal:
string += "QUADRIPEDAL "
if self.flying:
string += "FLYING ROBOT "
if self.wheeled:
string += "ROBOT ON WHEELS
"
else:
string += "ROBOT
"
if self.traversal:
string += "Traversal modules installed:
"
for module in self.traversal:
string += "- " + str(module) + "
"
if self.detection_systems:
string += "Detection systems installed:
"
for system in self.detection_systems:
string += "- " + str(system) + "
"
return string
class BipedalLegs:
def __str__(self):
return "two legs"
class QuadripedalLegs:
def __str__(self):
return "four legs"
class Arms:
def __str__(self):
return "four legs"
class Wings:
def __str__(self):
return "wings"
class Blades:
def __str__(self):
return "blades"
class FourWheels:
def __str__(self):
return "four wheels"
class TwoWheels:
def __str__(self):
return "two wheels"
class CameraDetectionSystem:
def __str__(self):
return "cameras"
class InfraredDetectionSystem:
def __str__(self):
return "infrared"
これで、このインターフェイスに従った複数の種類のBuilderを実装することができます。
例えば、アンドロイドや自律走行車のためのBuilderです。
from abc import ABC, abstractmethod
class RobotBuilder(ABC):
@abstractmethod
def reset(self):
pass
@abstractmethod
def build_traversal(self):
pass
@abstractmethod
def build_detection_system(self):
pass
同じメソッドを実装しているのに、その下のオブジェクトの構造が本質的に異なっていて、エンドユーザーはその構造の特殊性を扱う必要がないことに気づきましたか?
もちろん、足と車輪の両方を持つことができるRobot
を作ることもでき、ユーザーはそれぞれを別々に追加しなければなりませんが、それぞれの「パーツ」に対して適切なモジュールを1つだけ追加する、非常に特殊なビルダーを作ることもできます。
それでは、AndroidBuilder
を使ってアンドロイドを作成してみましょう。
class AndroidBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.bipedal = True
self.product.traversal.append(BipedalLegs())
self.product.traversal.append(Arms())
def build_detection_system(self):
self.product.detection_systems.append(CameraDetectionSystem())
class AutonomousCarBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.wheeled = True
self.product.traversal.append(FourWheels())
def build_detection_system(self):
self.product.detection_systems.append(InfraredDetectionSystem())
このコードを実行すると、次のような結果が得られます。
builder = AndroidBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
次に、AutonomousCarBuilder
を使って車を作ってみましょう。
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
このコードを実行すると、次のような結果が得られます。
builder = AutonomousCarBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
この初期化コードは、以前の雑然としたコンストラクタと比べると、よりクリーンで読みやすくなっています。
また、必要なモジュールを柔軟に追加することができます。
もし私たちの製品のフィールドが比較的標準的なコンストラクタを使用するなら、特定のビルダーを管理するためのディレクターと呼ばれる人を作ることもできます。
ROBOT ON WHEELS
Traversal modules installed:
- four wheels
Detection systems installed:
- infrared
このコードを実行すると、次のような結果が得られます。
class Director:
def make_android(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
def make_autonomous_car(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
director = Director()
builder = AndroidBuilder()
print(director.make_android(builder))
とはいえ、小さくて単純なクラスではBuilderパターンはあまり意味がありません。
なぜなら、クラスを構築するためのロジックを追加すると、さらに複雑になってしまうからです。
しかし、多層ニューラルネットのような、多数のフィールドを持つ大きくて複雑なクラスでは、Builderパターンは救世主となる。
解決策
Prototypeデザインパターンは、オブジェクトのコピーの問題を、オブジェクト自身に委ねることで解決します。
コピー可能なオブジェクトはすべて clone
と呼ばれるメソッドを実装し、それを使って自分自身の正確なコピーを返さなければなりません。
それでは、すべての子クラスに共通の clone
関数を定義し、親クラスから継承してみましょう。
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
また、先ほどの例のように単にフィールドを代入するのではなく、 deepcopy
関数を使用することもできます。
from abc import ABC, abstractmethod
class Prototype(ABC):
def clone(self):
pass
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
obj = MyObject(self.field1, field2)
obj.performed_operation = self.performed_operation
return obj
Prototypeパターンは、たくさんのオブジェクトをインスタンス化するような大規模なアプリケーションでとても役に立ちます。
時には、既に存在するオブジェクトをコピーする方が、新しいオブジェクトをインスタンス化するよりもコストがかからないことがあります。
S
問題点
シングルトンは2つの主要な特徴を持つオブジェクトである。
- 最大1つのインスタンスを持つことができる
- プログラム内でグローバルにアクセス可能であること
これらの特性はどちらも重要ですが、実際には、これらの特性のどちらか一方しか持っていなくても、何かをシングルトンと呼んでいるのをよく耳にします。
インスタンスが1つしかないのは、通常、共有リソースへのアクセスを制御するための仕組みです。
例えば、2つのスレッドが同じファイルを扱う場合、2つのスレッドが別々にファイルを開くのではなく、シングルトンは2つのスレッドに一意のアクセスポイントを提供することができます。
グローバル・アクセスは、クラスが一度インスタンス化された後、そのクラスで作業するためには、その単一のインスタンスを渡す必要があるため、重要です。
インスタンス化されたクラスは、再びインスタンス化することはできません。
そのため、クラスを再びインスタンス化しようとしたときに、すでに持っているインスタンスと同じものを取得できるようにする方が簡単です。
問題
A
オブジェクトプール
問題点
私たちのプロジェクトにはクラスがあり、それを MyClass
と呼ぶことにします。
MyClass` は非常に便利で、短時間ではありますが、プロジェクト全体でよく使用されます。
しかし、そのインスタンス化と初期化には非常にコストがかかり、わずかな操作のために常に新しいインスタンスを作成する必要があるため、プログラムの実行速度が非常に遅くなります。
解決策
オブジェクトのプールを作成し、プール自体を作成するときにインスタンス化するようにする。
MyClass` 型のオブジェクトを使用する必要があるときはいつでも、プールからオブジェクトを取得して使用し、再び使用できるようにプールに戻すことになります。
もし、オブジェクトがある種のデフォルトの開始状態を持っている場合、解放することで常にその状態までリスタートします。
プールが空のままだと、ユーザー用に新しいオブジェクトを初期化しますが、ユーザーがそれを使い終わったら、再び使用できるようにプールに戻します。
それでは、まず MyClass
を定義してみましょう。
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
return deepcopy(self)
そして、それをテストしてみましょう。
from typing import Optional
class MetaSingleton(type):
_instance : Optional[type] = None
def __call__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance
class BaseClass:
field = 5
class Singleton(BaseClass, metaclass=MetaSingleton):
pass
これは基本的な実装であり、実際にはこのパターンをSingletonと一緒に使用して、グローバルにアクセス可能な単一のプールを提供することができることに注意してください。
このパターンの有用性は、ガベージコレクタを使用する言語では議論されていることに注意してください。
そのような言語では、メモリだけを占有するオブジェクトの割り当ては比較的安価になる傾向がある。
一方、オブジェクトへの多くの「生きた」参照は、GCがすべての参照に目を通すため、ガベージコレクションを遅くする可能性がある。
ソリューション
W