Google ClassroomGoogle Classroom
GeoGebraGeoGebra Classroom

抽象契約による構造改革でWinWinにする。

このワークシートはMath by Codeの一部です。
前回は、 数を画面に表示するエディタの例を使って、サブクラスに生成の詳細をまかせる「下請け工場」FactoryMethodパタン、大量表示や種別の増加にもサブクラスを増加せずに対応できる「コピー屋さん」Prototypeパタンを学びました。 今回は抽象契約、AbstractFactoryです。 和訳すると抽象工場になりますが、抽象工場は物を作らないので工場とは言えません。 ただの作るものを抽象的に定めた契約です。だから、抽象契約としました。 どういうことでしょうか。 これが必要になる状況のお話しから始まります。 ******** ユーザーが増えると、エディタへの要求も増えます。 最初は「数を画面に表示する」だけでしたが、 「演算記号」も画面に表示してほしいという声も聞こえます。 さらに、表示モードの要求も出ています。 通常は今まで通りの「テキスト」でいいけど、印刷用には「美しい分数をTeX(テフ)形式」で表示したいという声も聞こえてきます。 ここで大問題が発生します。 部品の種類(縦軸):数、演算記号が1種類から2種類に増える。 表示のモード(横軸):テキストモードかTeXモードかを選べる。 これらが2×2=4倍と、「かけ算」で増えていくのです。 もし普通の下請け工場(Factory Method)のままでこれに対応しようとすると、「テキスト用の数工場」「TeX用の数工場」「テキスト用の等式工場」……と、工場クラスが倍々ゲームで爆発してしまいます。 これをすべて画面工作員(GraphicTool)に丸投げにしたらどうなるでしょうか。 学習させて、手作業で組み替えさせるにはコストが高すぎます。 かといって、かけ算で増える要求すべてをフレームワークの中で対応するバージョンアップは不可能です。 #ブラック企業が作ったフレームワークの画面工作員の働き方の例 class GraphicTool: def on_mouse_click(self, canvas, current_mode, element_type): # 画面工作員が、モードと種類の組み合わせ(掛け算)をすべて知っていなければならない if current_mode == "Text": if element_type == "Number": element = TextNumber("2026") elif element_type == "Operator": element = TextOperator("+") elif current_mode == "TeX": if element_type == "Number": element = TexNumber("2026") elif element_type == "Operator": element = TexOperator(r"\sum") canvas.add(element) もちろん、ユーザーが働くわけではないから、そんなの知ったこっちゃないかもしれません。 しかし、こういうべタなフレームワークのバージョンアップをいちいちしているようでは、 フレームワークの更新にかからる手間もかかり、バグも増えるだろうし、 ユーザーがフレームワークを手に入れるための値段の高くなるかもしれません。

1.抽象契約にお願いしよう

そこで登場するのが抽象契約です。 工作員にインスタンスの種類と表示が変わるたびにそれにあわせて、画面を作り替えることから 脱却してもらいましょう。 それが、抽象契約(AbstractFactory)です。 互いに関連したり依存しあうオブジェクト群を、 「その具象クラスを明確にせずに生成するためのインターフェースを提供する」とGOF本にはあります。 今回の例にあてはめると、 「どっちの表示方法かを明確にしないで部品を作るための契約クラスを作る」ことになるのではないでしょうか。 だから、いきなり2軸の両方では考えないことにします。 まずは、フレームワークは、部品メニューの契約と、契約書通りの作業の実行だけにすることにします。 そこで、急にTex形式の表示ではなく、テキスト形式の表示についての構造改革をしました。 それが次のコードです。 <フレームを軽くして、ユーザーと契約する> # ========================================== # 【クラスC】抽象契約(部品メニュー契約) # ========================================== class AbstractExpressionContract:     """【抽象契約】具体的な表示モードには関与せず、         必要な部品のラインナップだけを規定する。"""     def create_number(self, value):         raise NotImplementedError("契約に従って、数を返す仕組みを作ってください")     def create_operator(self, symbol):         raise NotImplementedError("契約に従って、演算子を返す仕組みを作ってください") # ========================================== # 【クラスA】画面工作員(部品契約画面に情報を入力して実行ボタンを押すだけ) # ========================================== class GraphicTool:     def __init__(self, contract_component: AbstractExpressionContract):         # 外の工場から届く「部品契約画面」         self.contract = contract_component     def on_mouse_click(self, editor_canvas):         print("[GraphicTool],clicked", end="")         # 契約書に情報を入力して実行ボタンを押すだけ(カプセル化)。         editor_canvas.add(self.contract.create_number("20"))         editor_canvas.add(self.contract.create_operator("+"))         editor_canvas.add(self.contract.create_number("80"))         editor_canvas.add(self.contract.create_operator("="))         editor_canvas.add(self.contract.create_number("100")) # エディタのキャンバス(擬似的な画面) class Canvas:     def add(self, element):         print(f"", end="")         element.draw() # ========================================== # 【カスタマイズ】利用者が作ったコード # ========================================== #テキスト表示用契約工場 --- class TextNumber:     def __init__(self, v): self.v = v     def draw(self): print(f" {self.v}", end="") class TextOperator:     def __init__(self, s): self.s = s     def draw(self): print(f"{self.s}", end="") class TextModeFactory(AbstractExpressionContract):     """契約を守る工場"""     def create_number(self, value): return TextNumber(value)     def create_operator(self, symbol): return TextOperator(symbol) canvas = Canvas() print("\n--- 1. 【テキストモード契約】で画面工作員を動かす ---") text_contract = TextModeFactory() # テキスト用の工場(契約の具現化) music_tool_text = GraphicTool(contract_component = text_contract) music_tool_text.on_mouse_click(canvas) [OUT] --- 1. 【テキストモード契約】で画面工作員を動かす --- [GraphicTool],clicked 20+ 80= 100 これで、画面工作員はただ、情報を入れるだけで、数式をテキストモードで画面に表示できました。 <Texモード対応> こうなると、Texモード対応は簡単にできます。 (部品メニュー契約)にあう部品と工場をユーザーが作ってくればよいのです。 # --- :TeX表示用の部品と、その契約を履行する工場 --- class TexNumber:     def __init__(self, v): self.v = v     def draw(self): print(f"${self.v}$ ", end="") class TexOperator:     def __init__(self, s): self.s = s     def draw(self): print(f"$\\pm {self.s}$", end="") class TexModeFactory(AbstractExpressionContract):     """TeXモードの契約を果たす工場"""     def create_number(self, value): return TexNumber(value)     def create_operator(self, symbol): return TexOperator(symbol) print("\n--- 2. 【TeXモード契約】へ一発切り替え ---") tex_contract = TexModeFactory() # TeX用の工場へ差し替え music_tool_tex = GraphicTool(contract_component = tex_contract) music_tool_tex.on_mouse_click(canvas) これだけを追加して、実行するだけです。 [OUT] --- 1. 【テキストモード契約】で画面工作員を動かす --- [GraphicTool],clicked 20+ 80= 100 --- 2. 【TeXモード契約】へ一発切り替え --- [GraphicTool],clicked$20$ $\pm +$$80$ $\pm =$$100$ 両方動きました。 このように、部品を抽象化して、作業を外注するフレームワークにすることで、 表示モードが増えても、簡単に対応できました。 もしも、モードがさらに明朝体指定表示、ゴシック体指定拡大表示など、…増えたとして、 ユーザーが欲しいものを付け足すだけで、同じフレームワークで対応できるということですね。

2.振り返り

抽象的な部品契約のクラスと、その契約にあう部品が作れる工場で作った契約書を実行することで、 自社に工場のない、元請け会社は難局を乗り越えました。 それは、部品の増大にだけ自社対応をして、 表示対応はユーザーのカスタマイズという名の下請け工場化をしたからです。 下請け工場法、ファクトリメソッドで部品が増えると、 その数だけ工場が増えるという現象が起きました。 そこで、コピー屋さん、プロトタイプ法に切り替えることで、作成をコピー屋さんに投げ、実際は部品自身が自己クローンを作るという面白い解決策で、工場の増大を防ぎましたね。 今回は、表示方法の数だけ工場が増えますが、ユーザに合わせてユーザが作る工場なので、 問題ないでしょう。 それに、コピー屋さんのパタンと同様に、現場作業の工作員へのしわ寄せはありませんでした。 また、フレーワークは表示方法の増大に対して、びくともしません。 つまり、社員も会社も軽量化しながら信頼性はゆるがないというWinWinの構造改革でしたね。 もちろん、部品の増大の要求、たとえば、分数部品、行列部品など、増えるでしょうが、数学ですから、 やたらと増えることはないでしょう。算数エディタであれば、分数部品を増やすくらいで対応できます。 だから、フレームワークのバージョンアップのたいした問題ではないでしょう。 GOF本では、部品の増大にも対応できるように、部品を数や文字列で「パラメータ化」するとかの案や、ユーザーが表示方法の増大ごとに工場を増やすのをおさえるために、コピー屋さんのパタンと合わせ技を使うということも書いてますが、これ以上はこのパターンに深入りしません。 完璧さを求め、有能性を誇示することが、 かえって学習者の疲労と混乱をまねくことがよくあるからです。 この抽象契約AbstractFactoryパタンの面白さは、 2軸構造を分解してから合成するという数学思考 と共通する発想方法だと思いますが、どうでしょうか。 さて、最後に次の課題にとりくみましょう。 課題:抽象契約AbstractFactoryパタンをgeogebraで実感するにはどうしらたよいでしょうか。 アプレットの名前を「抽象契約」とします。 f={(1,2),(2,3),(3,4),(4,5),(5,6)} a=checkbox b=slider(1,5,1) c=x(f(b)) #分子とします d=y(f(b)) #分母とします g=c/d #分数でも小数でも表示は未定です。ただの1つの値がgです。 ntext = "" + g #これで、ntextは小数を表すテキストになります。 Text1=If(a≟true,FractionText(g),ntext) このテキストをボールドで、サイズを大きめに設定しましょう。 すると、チェックボックスのオンオフで表示の小数と分数のモードが変えられます。 与える情報gは、bを動かせば連動して変わります。 geogebraには、Tableテキストなどのコマンドがありますから、リスト表示をパラメータ化して 変更したりすることができます。 これって、抽象契約と同じ発想かもしれませんね。 さて、最後に次の課題にとりくみましょう。 課題:抽象契約AbstractFactoryパタンをgeogebraで実感するにはどうしらたよいでしょうか。 アプレットの名前を「抽象契約」とします。 f={(1,2),(2,3),(3,4),(4,5),(5,6)} a=checkbox b=slider(1,5,1) c=x(f(b)) #分子とします d=y(f(b)) #分母とします g=c/d #分数でも小数でも表示は未定です。ただの1つの値がgです。 ntext = "" + g #これで、ntextは小数を表すテキストになります。 Text1=If(a≟true,FractionText(g),ntext) このテキストをボールドで、サイズを大きめに設定しましょう。 すると、チェックボックスのオンオフで表示の小数と分数のモードが変えられます。 与える情報gは、bを動かせば連動して変わります。 geogebraには、Tableテキストなどのコマンドがありますから、リスト表示をパラメータ化して 変更したりすることができます。 これって、抽象契約と同じ発想かもしれませんね。

抽象契約