下請け工場VSコピー屋さん
このワークシートはMath by Codeの一部です。
<コピーって何だっけ?>
プログラミングでコピーと言えば、何を連想しますか。
次は、pythonの対話画面です。
>>>a=12
>>>b=a
>>>a=123
>>> A=[1,2]
>>> B=A
>>> A.append(3)
>>> import copy
>>> AA=obj(12)
>>> BB=AA.copy.deepcopy()
>>> BB.data(123)
a=123のあとのb, A.append(3)のあとのBはどうなるでしょうか?
bは数値型なので、b=aの代入はaの変更前の値がbに渡されて新住所です。だからb=12のままです。
Bは参照型なので、B=Aの代入はデータの住所の代入です。Bは変更後もAを参照して[1,2,3]になるね。
数値型の代入は数値渡しです。
参照型の代入で番地のコピーなので高速が売りです。
BBはAAがクラスだとしたら、代入では参照されるだけで終わってしまいます。
そこで、copyライブラリを導入して、複製をすると、BBとAAは別物になります。
高速化以外の用途であれば、複製は大切な手段になることもあります。複雑なデータを一部分変えるときなどは、複製は重要です。
そういうこともあり、最近の言語では、オブジェクト複製のメソッド、
deepcopy、clone、copyなどが用意されています。
さて、今回は、デザインパターンの中にある
Factory Methodパタン (下請け工場)と
Prototypeパタン (コピー屋さん)
これが必要になるかもしれない場面と使い方・応用などの広がりを探っていきたいです。でも、その前に、
キーワードの確認をします。
<見慣れないだけのコトバによって思考を止めるな>
スーパークラス、
継承、
サブクラス化
コンストラクター、
プロパティ
メソッド
公開
クラスとインスタンス
インスタンスメソッド
クラスメソッド
カプセル化
多態性(ポリモーフィズム)
オーバーロード
オーバーライド
OOP
これらのキーワードは知っているでしょうか。
もし、知らなかったら
JAVAとかC#とかC++などクラス型の言語の入門書に
ざっと目を通してみてください。
ざっとでいいです。
デザインパターンの解説では、さらにコトバが文脈依存で出てきます。
ウィトゲンシュタインが「哲学探究」で書いたように
コトバの意味は言語ゲームであり、「コトバを使う」場面・環境・歴史と不可分です。
そこで会話する人々の方言ともいえます。
だれにも共通する定義があるはずがありません。
定義があるとしても、定義する人、辞書によって違うでしょう。
だから、知らないコトバが出てきたら、
それは、「プログラマーの方言だ」くらいに思いましょう。
知らないコトバを知らないコトバで「定義」しても説明になりません。
知っているのは偉いのか、わからないのは自分が悪いのか、
そんなことはありません。
身近な数学や音楽などのエディタ作成などのコードの実例とともに
「コトバを使う場面に親しむこと」で、
コトバの「ユースケース・意味・意義」を自分のものに
していこう。
「フレームワーク」、
「カスタマイズ」、
「クラスAはクラスBを知らない」、
「インスタンスのパラメータ化」
この4つのコトバが使われる場面を通して、2つのデザインパターンを学ぼう。
1.下請け工場にお願いします
<フレームワーク>
フレームワークは、アプリとライブラリの中間です。
ライブラリはだれかが作ってくれたクラスです。
目的に応じてインポートして使えますね。
アプリはライブラリを組み合わせてだれかが作ってくれた枠組みですが、
自分で変えることはできません。
フレームワークもアプリのようにだれかが作ってくれたり、自分で作る枠組みですが、
クラスの集まりなので、公開のプロパティやメソッドを作るだけではなく、自分なりの変更が可能です。
自分なりの変更というのが、カスタマイズですね。
<カスタマイズ>
フレームワークは、クラスでできてますから、
継承してサブクラスを作ったり、
オーバーライドによるメソッドの上書きをしたり、
自分が使いやすいように改造できます。
<クラスAはクラスBを知らない>
クラスAのコードの中に、クラスBの属性やメソッドの情報が
何もかかれてないと、クラスAの方からクラスBの情報を利用した動作は実行できません。
たとえば、数を画面canvasに追加表示する「フレームワーク」があり、
このフレームワークを利用者が「カスタマイズ」しているのが下の例です。
また、現状ではクラスAはクラスB(サブクラスも)の中身を知りません。
それでも、クラスAの役割は、画面に数を追加することなので画面クリックで数を追加して
表示しようとします。
(コード例)
# ==========================================
# ★「フレームワーク」はクラス集。A,Bからなる。
# ==========================================
# 【クラスA】ツール(イベント処理で数の画面追加)
# ==========================================
class GraphicTool:
def on_mouse_click(self, editor_canvas):
print("[GraphicTool],clicked ", end="")
# 自分で new しようとするが、何を作ればいいか分からないので
# 「子クラスに丸投げするメソッド」を用意する。
new_element = self.create_number()
editor_canvas.add(new_element)
def create_number(self):
raise NotImplementedError("子クラスで、自分が作る数を指定してください")
# ==========================================
# 【クラスB】数の超抽象クラス(自分を画面にかく)
# ==========================================
# 数の超抽象クラス
class AbstractNumber:
def draw(self):
raise NotImplementedError()
# キャンバス
class Canvas:
def add(self, element):
print(f"画面に追加されました: ", end="")
element.draw()
# ==========================================
# 【カスタマイズ】利用者が作ったコード
# ==========================================
# 数のサブクラスを作る
class Natural(AbstractNumber):
def __init__(self, numera):
self.numera = numera
def draw(self):
print(f"自然数 {self.numera})")
class Fraction(AbstractNumber):
def __init__(self, numera, denomi):
self.numera = numera
self.denomi = denomi
def draw(self):
print(f"分数 {self.numera}/{self.denomi})")
#====サブクラスのインスタンスごとにクラスが必要になる。
class NaturalGraphicTool(GraphicTool):
def create_number(self):
# ツールの中に具体的な「Natural」というクラス名が書かれてしまっている
return Natural(numera="2027")
class FractionGraphicTool(GraphicTool):
def create_number(self):
# ツールの中に具体的な「Fraction」というクラス名が書かれてしまっている
return Fraction(numera="20", denomi="27")
# ==========================================
# 実行
# ==========================================
canvas = Canvas()
print("\n--- 2027『専用ツール』をnewする ---")
# パラメータ化できないので、専用のツールクラスを毎回newする
music_tool_N = NaturalGraphicTool()
music_tool_N.on_mouse_click(canvas)
print("\n--- 分数20/27『専用ツール』をnewする ---")
# 別の数を扱いたくなったら、ツールごと切り替える必要がある
music_tool_F = FractionGraphicTool()
music_tool_F.on_mouse_click(canvas)
上の例は、
「下請け工場手法、FactoryMethod:インターフェースだけのクラスを作り、インスタンス化はサブクラスに任せる。」
というデザインパターンの例になっています。
だから、この例自体はこのままでは、何も問題がないどころか、
詳細をサブクラスにまかせるという
効率的で、柔軟性のある、構成になっているね。
たとえば、正負の整数、実数、複素数、4元数、…のようにサブクラスを追加して、
フレームワークをユーザーが拡張していけるからです。
ただし、数のサブクラス化をしていても、クラスAがクラスB(サブクラスも)を知らないので、
クラスBのサブクラスに応じたツール、つまりAのサブクラスを1対1で作る必要があります。
しかも、そのAのサブクラスのコードには、Bのサブクラスのインスタンス情報をハードコードしています。
サブクラスごとにインスタンスが1とか2程度で収まるような状況なら問題ないでしょう。
大量なインスタンスが必要になったとき、いちいちその数だけクラスをつくるという悲惨で
非現実的な状況はさけたいですね。
(悲惨な状況例)
# 値が変わるたびに、専用の「下請け工場(ツール)」をいちいち新しく定義しなければならない
class NaturalOneTool(GraphicTool):
def create_number(self): return Natural(1)
class NaturalTwoTool(GraphicTool):
def create_number(self): return Natural(2)
class NaturalThreeTool(GraphicTool):
def create_number(self): return Natural(3)
数ではなく、都道府県クラスがスーパークラスBであれば、そのサブクラスは47だけだから、47個のAのサブクラスをメモ帳レベルのエディタでコピペしてちょっとずつ手直しすることで作れるでしょう。
正しい唯一の解決策があらゆる状況にたしてあるわけではなく、その状況の特性に応じた解決策が必要になるのです。
2.コピー屋さんにお願いしよう
GraphicTool)は画面工作員
「クリックされたら、うちに来た『コピー屋さんC』にコピーをしてもらう(create)だけだ。どんな数が飛び出してくるかは知らんけどね。」
AbstractNumber)はクローンできて画面で見える数のこと
「私はただの数ではなく、コピー屋さんCに渡すためのクローン(clone)と画面表示(draw)できる姿を持つ。prototype_instance)で教えてもらえれば、数Bの自己クローン力によって得た複製をお渡しできます。」import copy
# ==========================================
# ★「フレームワーク」はクラス集。A,B,Cからなる。
# ==========================================
# 【クラスA】ツール(イベント処理で数の画面追加)
# ==========================================
class GraphicTool:
def __init__(self, creator_component):
# ★【AがBの生成の仕方を「知らない」例】
# 自身で new できないため、生成を代行してくれるコピー屋さん(クラスC)に来てもらう。
self.creator = creator_component
def on_mouse_click(self, editor_canvas):
print("[GraphicTool],clicked ",end="")
# Aは、これから作るのBのサブクラスの生成の仕方を「知らない」。
# ただ、外から渡された creator に「1つ作って」と頼むだけ(カプセル化)。
new_element = self.creator.create()
editor_canvas.add(new_element)
# ==========================================
# 【クラスB】数の超抽象クラス(自分を画面にかく、複製できる)
# ==========================================
class AbstractNumber:
def draw(self):
raise NotImplementedError("サブクラスで描画ロジックを実装してください")
def clone(self):
# Prototype パターンの肝となる複製処理
# 内部が単純な値だけなら、deepcopyではなくcopyで十分高速になる
return copy.copy(self)
# ==========================================
# 【クラスC】生成器(インスタンスをパラメータ化する対象)
# ==========================================
class C:
# コピー対象のインスタンスを変数(パラメータ)で受け取る。
def __init__(self, prototype_instance: AbstractNumber):
self.prototype = prototype_instance
def create(self):
# 保持しているインスタンスが自己複製したものを返す。
return self.prototype.clone()
# エディタのキャンバス(擬似的な画面)
class Canvas:
def add(self, element):
print(f"画面に追加されました: ", end="")
element.draw()
# ==========================================
# ★ここでは「カスタマイズ」はクラスBのサブクラス「自然数」と「分数」を作ること
# ==========================================
class Natural(AbstractNumber):
def __init__(self, numera):
#自然数のコンストラクター
self.numera = numera # 数の絶対値
def draw(self):
#自然数を画面にかくメソッド
print(f"自然数 {self.numera})")
class Fraction(AbstractNumber):
def __init__(self, numera, denomi):
#分数のコンストラクター
self.numera = numera # 数の分子
self.denomi = denomi # 数の分母
def draw(self):
#分数を画面にかくメソッド
print(f"分数 {self.numera}/{self.denomi})")
# 模擬的なエディタ画面の用意
canvas = Canvas()
print("\n--- 2027のインスタンスでパラメータ化して複製する ---")
N = Natural(numera="2027") #NはBのサブクラスのインスタンス
C_N = C(prototype_instance = N) #コピー屋さんCに原型をパラメータとしてNをわたす。
music_tool_N = GraphicTool(creator_component = C_N) #コピー屋さんCのデータを画面に渡すデータ
music_tool_N.on_mouse_click(canvas) #画面に追加されるとインスタンスはそれぞれの流儀で自分を描画する
print("\n--- 分数20/27のインスタンスで再パラメータ化 ---")
C_F = C(prototype_instance = Fraction(numera="20", denomi="27"))
GraphicTool(creator_component = C_F).on_mouse_click(canvas)
print("\n--- 分数1998/1999のインスタンスで再パラメータ化 ---")
C_F = C(prototype_instance = Fraction(numera="1998", denomi="1999"))
GraphicTool(creator_component = C_F).on_mouse_click(canvas)
for i in range(100):
N = C(prototype_instance = Natural(numera=i))
GraphicTool(creator_component = N).on_mouse_click(canvas)
3、振り返り
クラス関係図(UML)をおよそだけイメージしてみることで、振り返りをしよう。
ファクトリーメソッドパタン(下請け工場)では
クラスAとクラスBがあり、上位に並列にならぶ。
そして、Aからのサブクラスが多数並列に並ぶことができるが、
クラスBもそれに対応した数だけ並列に横並びになる。
すごく横長の関係図になることがわかるね。
一方、
プロトタイプパタン(コピー屋さん)では、
クラスAとクラスBの間にクラスCが割込み、上位に並ぶ。
そして、Aからのサブクラスが多数並列に横並びになることは同じになる。
クラス図は単純であり、Aのサブクラスが増えるだけだ。
クラスAの中にクラスCのインスタンスが取り込まれること、
クラスCのインスタンスは手ぶらでAに立ち寄るのではなく
Bのパラメータを持っているという情報の関係線が重要になる。
Bサブクラスインスタンス→パラメータとしてCが受け取る→ AがそのCを受け取る
Aがクリックされる→そこにいるCにコピーをたのむ→パラメータのBが自己複製→それをAが画面追加
という
コンパイル時の情報の流れB→C→Aに対し、
実行時は逆流A→C→Bでコピー指示が進む。
その果実をクリックしたユーザーが画面表示として見ることができたということだ。
つまり、責任の限定と移譲、情報のカプセル化という、
よく聞くコトバが生まれるわけが、この状況、データの流れから感じられるでしょう。
GOFの関連本はいろいろありますが、無理にその内容を調べてそれを実装することは
やりませんでした。興味のない場面の話を聞いてもイメージがわかないでしょう。
このページは興味がなくても、定義と手法をもれなく習得しなければならない人向けではなく、
数学や音楽やエディタに興味があり、典型例でパタンイメージを持ちたい人のものです。
穴だらけの記述かもしれません。完璧を求めてはいません。
最初から本質なんてないし、完全な説明など意味がないと考えます。
だから、イメージを強く持つために、最後にパタンの視覚化で終わります。
学びは、出来上がったもののコピー作業ではなく、
少しだけ背伸びして、自分に引き寄せて、新しい自分を作ることです。
課題:geobebraで、画面表示のオブジェクトわたす物の種類の切り替えを視覚化したい。
たとえば、
タイトルを「別種のものをわたす」にします。
n={1,2,3,4,5}
f={(1,2),(2,3),(3,4),(4,5),(5,6)}
a=checkbox()
b=slider(1,5,1)
A=If(a==true, n(b),f(b))
とすると、
aがチェックされれば、Aはb番目のx軸上の整数n
aのチェックがはずれると、Aはb番目のfの点が表示されます。
このアプレットを自分流に手直ししてみてみると面白いかも。