オブジェクト指向設計実践ガイドを読んだので基本部分をPythonでも書いてみた

記事タイトルとURLをコピーする

オブジェクト指向設計実践ガイド is

https://www.amazon.co.jp/dp/477418361X

その名の通りオブジェクト指向の原則に沿った設計を実践しながら学ぶという内容です。 私は今年の3月からCloud Automatorを開発するサービス開発課に配属になりましたが、これまでのプログラミング経験が乏しいこともあり、 オブジェクト指向というものがいまいち掴みきれませんでした。 そこで夏頃からこの書籍を読み始めたのですが、これまで頭の中でぼんやりしていた概念が丁寧に論理立てて整理できた感覚があり、とても勉強になりました。 オススメの書籍ですので、今回はこの書籍の基礎部分をPythonでご紹介したいと思います。

なぜPython?

私自身、普段の業務では主にRubyを利用していますが、 社内の他部署ではPythonが主に使われていて、新人研修で初めて学んだプログラミング言語もPythonでした。

Rubyではこの本のようにオブジェクト指向に関する書籍やwebなどの情報が豊富ですが、Pythonでオブジェクト指向となると、ネット上には初心者向けの情報があまりないと感じたので私がRubyで勉強した際に参考になったこの書籍をPythonの文法でも書いてみました。


あらゆる箇所を単一責任にする

まずはクラスやメソッドを単一責任にするところからはじめます。 単一責任とは何かというと、1つのクラスの役割を一言で説明できるような範囲にしておくということです。 なぜ単一責任でなければいけないのかというと、クラスやメソッドは再利用可能な部品であるべきだからです。 2つ以上の責任をもつ多機能なクラスやメソッドは、再利用しにくく、変更する時に影響が大きくなりがちになります。

例えば以下のようなGearクラスを考えます。 Gearクラスは自転車のギアを表現していて、チェーンリング、コグ、リム、タイヤなどのメンバを持っています。 たくさん自転車用語が出てきますが、実際のところGearクラスの役割はgear_inchesメソッドでギアインチを計算することだけというシンプルなクラスです。1

class Gear:
    def __init__(self, chainring, cog, rim, tire):
        self.__chainring = chainring
        self.__cog = cog
        self.__wheel = {
            "rim": rim,
            "tire": tire
        }

    @property
    def chainring(self):
        return self.__chainring

    @property
    def cog(self):
        return self.__cog

    @property
    def wheel(self):
        return self.__wheel

    def ratio(self):
        return self.chainring / float(self.cog)

    def gear_inches(self):
        return self.ratio() * (self.wheel["rim"] + (self.wheel["tire"] * 2))


# Use case
print(Gear(52, 11, 26, 1.5).gear_inches())
# → 137.0909090909091

メソッドを単一責任にする

メインの機能であるgear_inches メソッドでは、 ratioメソッドで計算した結果に車輪の直径をかけた結果を返します。 gear_inchesself.wheel["rim"] + (self.wheel["tire"] * 2)の部分が実は車輪の直径を計算している部分です。

つまりこのgear_inchesメソッドでは 1. 車輪の直径を計算して、 (self.wheel["rim"] + (self.wheel["tire"] * 2)) 2. それをratioにかける

という2つのことを行っていることになります。 これでは単一責任の原則に反しているので、1の役割を他のメソッドに分離させてみます。

    def ratio(self):
        return self.chainring / float(self.cog)

    def diameter(self):
        return self.wheel["rim"] + (self.wheel["tire"] * 2)

    def gear_inches(self):
        return self.ratio() * self.diameter()

このように書くことでgear_inchesメソッドの責任は「ratioとdiameterをかける」という1点のみになりました。 その代わりに、車輪の直径を計算する責任を持ったdiameterメソッドが新たに生まれています。

このようにメソッドレベルでの単一責任を徹底することで、コードの可読性が上がりました。 加えて、関数に分離してdiameterメソッドという名前を与えることで、 self.wheel["rim"] + (self.wheel["tire"] * 2)というコードの意味がわかりやすくなり、再利用もしやすくなりました。

クラスを単一責任にする

上記のコードはややわざとらしい書き方をしていたので、すでにお気づきの方もいるかもしれません。 現在のdiameterメソッドの機能は、インスタンス変数wheelの中の値だけを使って計算を行っているだけです。 そのため、wheelを辞書型のインスタンス変数ではなく、Wheelクラスとして分離すると、より単一責任な設計にできます。2

class Gear:
    def __init__(self, chainring, cog, rim, tire):
        self.__chainring = chainring
        self.__cog = cog
        self.__wheel = Wheel(rim, tire)

    @property
    def chainring(self):
        return self.__chainring

    @property
    def cog(self):
        return self.__cog

    @property
    def wheel(self):
        return self.__wheel

    def ratio(self):
        return self.chainring / float(self.cog)

    def gear_inches(self):
        return self.ratio() * self.wheel.diameter()


class Wheel:
    def __init__(self, rim, tire):
        self.__rim = rim
        self.__tire = tire

    @property
    def rim(self):
        return self.__rim

    @property
    def tire(self):
        return self.__tire

    def diameter(self):
        return self.rim + (self.tire * 2)


# Use case
print(Gear(52, 11, 26, 1.5).gear_inches())
# → 137.0909090909091

これで、diameterを計算するという責任を新たに作成したWheelオブジェクトに任せることができました。  

依存関係を管理する

しかしながら、このように複数のクラスが登場するようになると、クラス間の依存関係についても考える必要が出てきます。

例えば先ほどのコードでは、GearクラスのなかでWheelオブジェクトを生成しているために、Gearクラスは以下の点でWheelクラスに依存しています。

  1. Wheelクラスという名前のクラスが存在すること
  2. Wheelインスタンスの生成に必要な引数とその順番
  3. Wheelクラスがdiameterメソッドに応答すること

なのでWheelクラスの名前や引数の順序が変更されてしまうと、本来独立しているべきGearクラスのコードも変更する必要が出てきいます。

依存性の注入

この問題を解決するための方法は単純で、以下のように外部でWheelインスタンスを作成し、Gearクラスに渡せば1,2 の依存を解消できます。3

class Gear:
    def __init__(self, chainring, cog, wheel):
        self.__chainring = chainring
        self.__cog = cog
        self.__wheel = wheel

    @property
    def wheel(self):
        return self.__wheel

    def gear_inches(self):
        return self.ratio() * self.wheel.diameter()

        # 他のメソッドの定義は先ほどと同様


# Use case
wheel = Wheel(26, 1.5)
print(Gear(52, 11, wheel).gear_inches())
# → 137.0909090909091

引数の順番への依存を取り除く

これでgearクラスからwheelクラスの名前と引数への依存を取り除くことができました。 とはいえ、結局のところどこかでwheelインスタンスやgearインスタンスを生成する必要があることは変わりないので、どこかではwheelの引数や順番に関して知っていないといけません。

しかし、例えば以下のように、引数を辞書型で渡すようにすると、引数の順には依存しなくてよくなります。 加えて、dict.get("DICT_KEY", DEFAULT_VALUE)のように書くことで、万が一"DICT_KEY"が存在しない時にDEFAULT_VALUEを設定することもできます。

class Wheel:
    def __init__(self, args: dict):
        self.__rim = args.get("rim", 25)
        self.__tire = args.get("tire", 1)

Wheel({"rim": 26, "tire": 1.5})

これで、引数の順番という知識には依存しなくて済むようになりました。

書籍ではこのように辞書型を使っていましたが、 Pythonではさらに3.7から登場したdataclassを利用することでスマートにクラスを定義できます。

import dataclasses

@dataclasses.dataclass(frozen=True)
class Wheel:
    rim: float = 25
    tire: float = 1

    def diameter(self):
            return self.rim + (self.tire * 2)

print(Wheel(rim=26, tire=1.5).diameter())
# → 29.0

最後に

ここまでで、書籍では全9章あるうちの3章までの重要部分を簡単にご紹介しました。 書籍ではさらにダックタイピング、継承、コンポジション、オブジェクト指向設計のためのテストなど、Pythonでも役に立つテクニックがたくさん紹介されています。 書籍に登場するコードはRubyですが、そもそもRubyとPythonの文法がそこまで大きく違わないということもあり、Pythonしか知らない方でも文章を読めばある程度理解できる構成になっていると思います。 この記事を読んでいただき参考になりそうでしたら、ぜひ書籍の方もチェックしてみてくださいね。


  1. ギアインチを簡単に計算できることは自転車乗りにとってはとても役立つそうです。 
  2. 実は書籍ではこの段階ではクラスに分けず、いつでも分離できるような準備だけしておき、のちに機能追加で分離する必要が出てきたら分離するというテクニックを使っています。気になる方はぜひ書籍の方も読んでみてください。 
  3. Wheelクラスのコードは先ほどと同様。