未来の私へ送るデザインパターン 1/24 [オブジェクト指向]

このシリーズは、この内容を忘れた未来のcacaponが読んだときに、
自力でそれを使ったプログラムが自作できることを目的として作られました~

こちらの記事は、以下を主に参考に作成しております♪
残りは、cacaponの記憶の片隅にある情報がソース元になります~

www.techscore.com

オブジェクト指向とは?

オブジェクト指向は、設計するときの考え方の一つです~
「設計図」から「オブジェクト」を作り出し、
「オブジェクト」同士のやり取りで処理を作っていくイメージになります~

設計図の事をクラスと言ったり、
オブジェクトの事をインスタンスということもあるので注意です~

メリットとしては、手続き型で設計したときと比べて
生産性が高いこと、再利用性が高いことが挙げられます~

なぜデザインパターンのついてのブログでオブジェクト指向をまとめているかというと、
デザインパターンオブジェクト指向の子どもみたいなものだからです~

オブジェクト指向のメリットは上述の通りですが、
一見オブジェクト指向で設計しても生産性とか再利用性が無いプログラムが続出しました~
(現にcacaponの作ったプログラムもその気が強いです…)

そんな人たちのために、4人の頭の良い方が、
「よくある問題をオブジェクト指向で設計する際の解答例を23個作ったぜ!」
って作られたのがデザインパターンなのです~

f:id:cacapon:20200130003111p:plain

例題

ボタンが押されると紙コップが出てくる機械を考えています~
それをプログラムで表現してみてください~
あ、従業員さんが補充できるように「補充出来る機能」も忘れずに~


オブジェクト指向を使わないで作ったプログラム

手続き型のやり方で作っていきたいと思います~
因みに、手続き型というのは、手順に沿って処理をやっていく形式の設計ですね~

def getRemainingPaperCup():
    # 残り個数が記載されているPaperCup.txtから残り個数を取得します。
    with open('PaperCup.txt', mode='r') as f:
        remainingcup = int(f.read())
    return remainingcup


def setRemainingPaperCup():
    # 残り個数をPaperCup.txtに保存します。
    with open('PaperCup.txt', mode='w') as f:
        f.write(str(haspapercup))


haspapercup = getRemainingPaperCup()
command = {
    '1': 'eject',
    '2': 'charge',
    '3': 'exit'
}


while(True):
    num = input('コマンドを入力してください \n1:カップを出す 2:カップを補充する 3:終了\n>>>')
    if command.get(num) == 'eject':
        if haspapercup == 0:
            print('紙コップがありません\n')
        else:
            haspapercup -= 1
            setRemainingPaperCup()
            print('紙コップを出しました')
            print('残り{}個です。'.format(haspapercup))
    elif command.get(num) == 'charge':
        try:
            chargecount = int(input('補充する数を1~100の間で設定してください\n>>>'))
        except ValueError:
            print('不正な入力です')
        else:
            if chargecount <= 0 or chargecount > 100:
                print('個数が正しくありません。最初からやり直してください')
            else:
                haspapercup += chargecount
                setRemainingPaperCup()
                print('紙コップを{}個補充しました'.format(chargecount))

        pass
    elif command.get(num) == 'exit':
        print('終了します')
        break
    else:
        print('入力エラーです、もう一度入力してください\n')


その後…

ちょっとごちゃごちゃしてますが、ちゃんと動いていそうです~
ただ、しばらくたつと紙コッププログラムを使っていた人たちから…

  • 紙コップを出す以外に、お湯付で紙コップを出す機能もつけたい
  • サービスエリアの紙コッププログラムは消費が激しいので、
    ここだけ1000個まで補充できるようにしてほしい
  • 当たりくじ付きの紙コップつけれない?

などなど、いっぱい改善案が出てきました~
ありがたいことなのですが、手続き型で作っていると、
それを元に作り直す手間が凄いことになりそうです~


オブジェクト指向を使って作ったプログラム

というわけで、オブジェクト指向っぽく作り直しました~
現時点でcacaponが理解している形がこのプログラムになります~

オブジェクト指向に沿って、紙コップマシンの設計図として
PaperCupMachineクラスを作成しました~
このクラスから生成する場合は、pythonの場合
objectname = ClassName()
という風に書くとオブジェクトが出来上がります~

設計図からオブジェクトを生成した後は、
従来通りコマンドで選択出来る形をとっていますが、…ここもオブジェクトにしていきたいところです~

class PaperCupMachine():
    def __init__(self):
        # 残り個数をPaperCup.txtから取得します。
        with open('PaperCup.txt', mode='r') as f:
            self.remainingcup = int(f.read())

        # 最大容量を設定します。
        self.maximumcapacity = 100

    def eject(self):
        # 紙コップを一個出します。
        if self.remainingcup == 0:
            print('紙コップがありません')
        else:
            self.remainingcup -= 1
            self._setRemainingPaperCup()
            print('紙コップを出しました')
            print('残り{}個です。'.format(self.remainingcup))

    def charge(self, chargecount):
        # 入力された個数をチャージします。
        if self._inputchecker(chargecount):
            print('不正な入力です')
            return

        if self._chargechecker(chargecount):
            return
        else:
            self.remainingcup += chargecount
            self._setRemainingPaperCup()
            print('紙コップを{}個補充しました'.format(chargecount))

    def _inputchecker(self, data):
        # inputしてはいけないint型以外の型入力ならTrueを返します。
        return not type(data) is int

    def _setRemainingPaperCup(self):
        # 残り個数をPaperCup.txtに保存します。
        with open('PaperCup.txt', mode='w') as f:
            f.write(str(self.remainingcup))

    def _chargechecker(self, chargecount):
        # 以下の場合、Trueを返します。
        # 追加した場合に最大容量を超える時
        # 与えられた追加個数が不正な値の場合True
        if chargecount < 0:
            print('入力値は最大容量以下の自然数でお願いします')
            return True
        if self.remainingcup + chargecount > self.maximumcapacity:
            print('容量を超えたため、入力を受け付け出来ませんでした')
            return True
        return False


papercupmachine_object = PaperCupMachine()

command = {
    '1': 'eject',
    '2': 'charge',
    '3': 'exit'
}

while(True):
    in_data = input('コマンドを入力してください \n1:カップを出す 2:カップを補充する 3:終了\n>>>')

    if command.get(in_data) == 'eject':
        papercupmachine_object.eject()
    elif command.get(in_data) == 'charge':
        try:
            chargecount = int(input('補充する数を入力してください。\n>>>'))
        except ValueError:
            print('補充する数が正しくありません。')
        else:
            papercupmachine_object.charge(chargecount)
    elif command.get(in_data) == 'exit':
        print('終了します')
        break
    else:
        print('入力エラーです、もう一度入力してください\n')


その後...の対応例

  1. 紙コップを出す以外に、お湯で紙コップを出す機能もつけたい
    →クラスの中に def で機能追加することができます~
  2. サービスエリアの紙コッププログラムは消費が激しいので、
    ここだけ1000個まで補充できるようにしてほしい
    →紙コップマシン自体が容量を管理しているので、容量を変更することで対応します~
  3. 当たりくじ付きの紙コップつけれない?
    →これは 従来のeject機能に当たりくじ機能を追加すると出来そうです。
import random


class PaperCupMachine():
    def __init__(self):
        # 残り個数をPaperCup.txtから取得します。
        with open('PaperCup.txt', mode='r') as f:
            self.remainingcup = int(f.read())

        # 最大容量を設定します。
        self.maximumcapacity = 100

    def eject_hotwater(self):
        print('こちらの形式はお湯を出せません。')

    def eject(self):
        # 紙コップを一個出します。
        if self.remainingcup == 0:
            print('紙コップがありません')
            return False  # 失敗
        else:
            self.remainingcup -= 1
            self._setRemainingPaperCup()
            print('紙コップを出しました')
            print('残り{}個です。'.format(self.remainingcup))
            return True

    def charge(self, chargecount):
        # 入力された個数をチャージします。
        if self._inputchecker(chargecount):
            print('不正な入力です')
            return  # 成功

        if self._chargechecker(chargecount):
            return
        else:
            self.remainingcup += chargecount
            self._setRemainingPaperCup()
            print('紙コップを{}個補充しました'.format(chargecount))

    def _inputchecker(self, data):
        # inputしてはいけないint型以外の型入力ならTrueを返します。
        return not type(data) is int

    def _setRemainingPaperCup(self):
        # 残り個数をPaperCup.txtに保存します。
        with open('PaperCup.txt', mode='w') as f:
            f.write(str(self.remainingcup))

    def _chargechecker(self, chargecount):
        # 以下の場合、Trueを返します。
        # 追加した場合に最大容量を超える時
        # 与えられた追加個数が不正な値の場合True
        if chargecount < 0:
            print('入力値は最大容量以下の自然数でお願いします')
            return True
        if self.remainingcup + chargecount > self.maximumcapacity:
            print('容量を超えたため、入力を受け付け出来ませんでした')
            return True
        return False


class PaperCupMachineVersionHotWater(PaperCupMachine):
    def eject_hotwater(self):
        # 紙コップを出した後、お湯を入れます。
        super().eject()
        print('お湯を入れました')


class PaperCupMachineVersionBigCapacity(PaperCupMachine):
    def __init__(self):
        super().__init__()
        self.maximumcapacity = 1000


class PaperCupMachineVersionRuckyCup(PaperCupMachine):
    def eject(self):
        # ランダムで当たりくじが出ます。
        if not super().eject():  # 紙コップを出すのが失敗したら
            # 終了する
            return
        if random.random() < 0.1:  # とりあえず10%
            print('当たりが出ました!おめでとう!')


papercupmachine_object = PaperCupMachineVersionRuckyCup()  # 生成するマシンは選択する。

command = {
    '1': 'eject',
    '2': 'charge',
    '3': 'ejecthotwator',
    '4': 'exit'
}

while(True):
    in_data = input('コマンドを入力してください \n1:カップを出す 2:カップを補充する 3:お湯を出す 4:終了\n>>>')

    if command.get(in_data) == 'eject':
        papercupmachine_object.eject()
    elif command.get(in_data) == 'charge':
        try:
            chargecount = int(input('補充する数を入力してください。\n>>>'))
        except ValueError:
            print('補充する数が正しくありません。')
        else:
            papercupmachine_object.charge(chargecount)
    elif command.get(in_data) == 'ejecthotwator':
        papercupmachine_object.eject_hotwater()
    elif command.get(in_data) == 'exit':
        print('終了します')
        break
    else:
        print('入力エラーです、もう一度入力してください\n')

機能を実現するために使用したのは、「継承」という機能になります~
元の機能を引き継いで、新しく機能を追加したり、同じ名前の機能を上書きしたり※することができます♪

  • class PaperCupMachineVersionHotWater(PaperCupMachine):
  • class PaperCupMachineVersionBigCapacity(PaperCupMachine):
  • class PaperCupMachineVersionRuckyCup(PaperCupMachine):

が、継承して新しく作ったクラスですね~
地味に元のクラスをいじったり、選択コマンドを増やしたりもしてはいるのですが、
大元はほぼ変えずに新しく作れるのが、きっと生産性が高い、再利用性が高い
といわれる所以なのではないかと思います~

※…オーバーライドっていうらしいです~

cacaponのメモ

  • 公開している機能を継承先のクラスで追加したときは、
    継承元のクラスにも同名の機能を追加したほうが良さそう~
    例えば、PaperCupMachineオブジェクトでお湯を出そうとすると、
    機能がないためにエラーになりました~
    空の機能でもいいから作っておくと、操作側をいじらなくて済みます~

  • 継承先で同名の機能を定義すると、元の内容が無くなるっぽいです~
    例えば、BigCapacityでsuper().__init__()しないと、
    self.remainingcupが定義されていないためエラーになった。

  • 当たり付を実装するうえで、紙コップが出ない時は当たりくじが出ないようにしたかったのですが…
    ejectで紙コップが出ない場合でも、次の当たりくじ判定処理が動いてしまいました~ 対応策として、継承元クラスをいじって成功失敗判定を返すようにしたけど、
    作りとして微妙と感じています~ もっと良いやり方ないかなぁ…

  • 個数の管理をPaperCup.txtありきな形式なため、
    保存している形式が変わったり、不正な値が保存されてしまっているとうまく動きません~
    確認であったのは、BigCapacityから通常に戻した場合に、個数がそのままになっておりました…
    オブジェクト毎に値を保持できるようにしたりできるといいなぁ…

というわけで、デザインパターンを学ぶ前のcacaponはこんな感じになりました~
24回終わった後にはメモした疑問などは解決出来ていたらいいなぁと思います~