pythonによるデザインパターン[Memento]

# Intro

この文章は結城 浩さんの「Java言語で学ぶデザインパターン入門」を、pythonで実装してみたサンプルコードです。

筆者の環境は以下の通りです。Python 3.6.3

まだ修行中の身なので間違いがあると思いますがご了承ください。 今回は「Memento」です。Mementoと聞くとクリストファー・ノーラン監督の映画を思い出してしまうのは、私だけじゃないはず…

# Summarise

本書のp270によるとMementoとは

インスタンスの状態を表す役割を導入して、カプセル化の崩壊(クラスの内部構造に依存したコード)に陥ることなく保存と復元を行う

とあります。例えばMementoパターンを用いると、プログラムに対して

  • undo(やり直し)
  • redo(再実行)
  • history(作業履歴の作成)
  • snapshot(現在状態の保存)

を行うことができます。

# Practice

## Specification

以下のような「フルーツを集めていくサイコロゲーム」を作成します。

  • ゲームは自動的に進む
  • 主人公はサイコロを振り、その目が次の状態を決定
  • 1の目が出るとお金が増える
  • 2の目が出るとお金が半分になる
  • 6の目が出るとフルーツがもらえる
  • お金がなくなったら終了

また、所持金が増えてきたらclass Mementoのインスタンスを作って、「現在の状態」(お金とフルーツ)を保存します。一方で、所持金が減ってきたら前に作っておいてclass Mementoのインスタンスを使って、「前の状態」(お金とフルーツ)に戻ります。

## Explanation

最初に状態を保存するclass Memento を作成します。

お金とフルーツを保存するので、Fieldはint型のmoneyとList型のfruitsを持ちます。またお金を取得するメソッド def get_money 、フルーツを追加する def add_fruits 、フルーツを取得するdef get_fruits を作成します。難しくないですね。

もしかしたらclass Memento をわざわざ作らずに、後で作る class Gamer (ゲームを示すクラス)の中にこの機能を入れ込めば良いと思うかもしれません。しかし、疎結合になることを考えるとclass Memento を別に作成した方がいいです(後でまた出てきます)。

次にゲームを表すクラスclass Gamer を作成します。

金を取得するメソッド def get_money 、フルーツを追加する def add_fruits 、フルーツを取得するdef get_fruits (時々おいしい〇〇を取得できる)を実装し、ゲームのロジックが書いている def bet を実装します。def bet はすこし複雑ですが、ランダムにサイコロを振り、その目によって以下の処理を行うことを実装しているだけです。

  • 1の目が出るとお金が増える
  • 2の目が出るとお金が半分になる
  • 6の目が出るとフルーツがもらえる
  • それ以外は何も起きない

重要なのはclass Mementoを作成するdef create_memento と、引数で渡されたclass Memento から前の状態に戻る def restore_memento です。ここでsnapshot(現在状態の保存)とundo(再実行)を実装しています。

class Gamerの中にdef create_mementodef restore_memento があるということは、class Gamer を介してのみ class Memento を変更できます。逆にいうとclass Gamer 以外はclass Memento を変更できません

最後にmain関数を実装します(以下に示すのは簡略化したもの)。

class Gamerを作成し、class Mementoも作成し、100回のループでゲームを実行します。前に保存したclass Memento (前の状態)の所持金によっては、現在の状態を保存したり、前の状態から復帰したりします。

重要なのはmain関数からはclass Memento を直接変更してはいないが、いつclass Memento を作成・復帰するか決定している点です。これは先ほどのclass Gamer とは対照的です。このように役割を分担することで疎結合になっています。役割分担をきちっとすることで、例えば

  • 複数のステップのundoを行うように変更したい
  • undoする時にも現在の状態をsnapshotするように変更したい

のような時でも、class Gamer を変更することなく、ただmain関数を変更すればいいだけです。これが一番最初に言った、状態を保存する機能をclass Gamer に入れ込まずに、別に作成したclass Memento に実装した理由です。

## Sample Code

python3 memento.py==== 0
現状:[money = 100, fruits = []]
何も起こりませんでした
所持金は100円になりました
==== 1
現状:[money = 100, fruits = []]
何も起こりませんでした
所持金は100円になりました
....(以下略)

# Conclusion

オブジェクトの状態を保存するときは、別のクラス(今回はclass Memento)を作成しそこに状態を保存します。そして、そのクラスを他のクラス(今回はclass Gamer)から操作し、いつ保存・復帰するかをまた別の関数(今回はdef main)で決定します。この構成によって、美しく設計できることを学びました😤

私の経験では、まだredo,undoなどの、オブジェクトの状態の保存を伴うようなプログラムを作成したことがないのですが、もし次出くわしたらこのパターンを使いたいと思います。

# ref

--

--