へっぽこびんぼう野郎のnewbie日記

けろけーろ(´・ω・`)! #vZkt8fc6J

オブジェクト指向で書いてて思ったことについてだらだらと書いてみる

class Data:
    def __init__(self):
        self.a = 13
        self.b = 4
        self.c = 8
        self.d = 12


data = Data()

↑のようなデータがあるときに、↓のようなコードをたまに見かけるときがある。↑のコードは失敗とかじゃなくて、モックだと思ってください。

class Calculator0:
    def __init__(self, g):
        self.g = g

    def calc(self, data):
        a = data.a if data.a % 2 == 0 else 1

        added = a + data.b
        subtracted = data.c - data.d
        calced = added / subtracted
        multiplied = calced * self.g
        return multiplied + 1


calculator0 = Calculator0(g=2)
print('0', calculator0.calc(data))

実際にはこんな単純じゃなくて、 + とか - のとこはもっと複雑なロジックで、 calcメソッド は、もっと行数がずーーーっと長く続いているもんだと思ってください。

個人的にこう書いてしまうときは、最終形がどうなるのかあんまりわかってなくて、 AしてBしてCしたらDになる みたいな感じの思考のときが多い気がしている。オブジェクト指向じゃなくて、手続き型っぽい感じでコードを書いてるときにそうなりがち。

そういうときに下手にコードをオブジェクト指向っぽくしようとして、

class Calculator1:
    def __init__(self, g):
        self.g = g

    def add(self, a, data):
        self.added = a + data.b

    def subtract(self, data):
        self.subtracted = data.c - data.d

    def multiple(self, b):
        self.multiplied = b * self.g

    def calc(self, data):
        a = data.a if data.a % 2 == 0 else 1

        self.add(a, data)
        self.subtract(data)
        calced = self.added / self.subtracted
        self.multiple(calced)
        return self.multiplied + 1


calculator1 = Calculator1(g=2)
print('1', calculator1.calc(data))

こんな感じにしてしまったことが、昔たまにあった。これはさっきよりも最悪で、一見名前がついてわかりやすくなったふうに見えて、 オブジェクトが依存しているかもしれない度 がものすごく上がっている。要はローカル変数だったものを、クラス内全体に広げてしまったことで、コードを読むときに他から影響を受けてないか確認する手間が増えている。仮に addメソッド の中で self.added に代入してなかったとしても、 calcメソッド の中で self.add が使われているので、 addメソッド へselfの値が変わっているかどうかを確認しに行かなければいけなくて、恐怖心が増加する。いわゆる、なんちゃってオブジェクト指向

じゃあ関数型っぽく書けばいいのかと思って、↓みたいにすると、

def return_1_if_odd(n):
    return n if n % 2 == 0 else 1


def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def multiple(a, b):
    return a * b


def wrap(g):
    def calc(data):
        return (
            multiple(
                (
                    add(return_1_if_odd(data.a), data.b) /
                    subtract(data.c, data.d)
                ),
                g
            ) +
            1
        )
    return calc


print('Functional', wrap(2)(data))

別にこれはこれでいいんだけど、そうするとオブジェクト指向の利点が消滅する。Pythonでやる意味ないのではって感じがする。

そういうわけで、↓のように書き直してみると……

class Util:
    @staticmethod
    def return_1_if_odd(n):
        return n if n % 2 == 0 else 1

    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def subtract(a, b):
        return a - b

    @staticmethod
    def multiple(a, b):
        return a * b


class GConfig:
    def __init__(self, g):
        self.g = g


class EasyCalculatedData:
    def __init__(self, origin_data):
        self.origin_data = origin_data

    @property
    def data(self):
        a = Util.return_1_if_odd(self.origin_data.a)

        added = Util.add(a, self.origin_data.b)
        subtracted = Util.subtract(self.origin_data.c, self.origin_data.d)
        return added / subtracted


class CalculatedData:
    def __init__(self, origin_data, config):
        self.origin_data = origin_data
        self.config = config

    @property
    def data(self):
        easy_data = EasyCalculatedData(self.origin_data)
        multiplied = Util.multiple(easy_data.data, self.config.g)
        return multiplied + 1


config = GConfig(g=2)
calculated_data = CalculatedData(data, config=config)
print('2', calculated_data.data)

正直題材がびみょい気がするので利点が見えにくいけど、こうすると拡張もできて、あんまり脳を使わずにコードを読むことができて嬉しいなぁと思っている。もしコードをコピペされても害があんまりなくておすすめ。

ただコードが短いやつしかないときはここまでやるのはやりすぎ感がある。短いときは一番最初のやつでもいいし、単にUtilの関数群として切り出すだけなのが手っ取り早いけど、長くなってきたときにこうすると分離したり別のクラスをプラグイン的に使えたりモックでテストできてレゴ的嬉しさもある(´・ω・`)

結局どうするのが正解なのかはよくわからない。

とりあえず2番目のやつは最悪なのでやってはいけないと思う(´・ω・`)