20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Pythonでゆっくり学ぶ「依存関係逆転の原則」

Last updated at Posted at 2020-08-14

「依存関係逆転の原則」についてのテキストは数多くありますが、

  • 理由がよくわからない
  • やり方がよくわからない

という人向けに、自分なりの「こう説明してくれればわかりやすかった」という記事を書いてみます。

github

つくるもの

四則演算をたしかめるツール。次のように結果をCLIに出力します。

$ python -m koboridip.main 8 2
8 + 2 = 10
8 - 2 = 6
8 * 2 = 16
8 / 2 = 4.0

言われた通りに作る。バージョン1

プロジェクト構成

.
└── koboridip
    ├── calculator.py
    └── main.py

ソース

calculator.py
class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        print(f'add: {self.a + self.b}')
        print(f'subtract: {self.a - self.b}')
        print(f'multiply: {self.a * self.b}')
        print(f'divide: {self.a / self.b}')
main.py
import sys
from koboridip.calculator import Calculator

if __name__ == '__main__':
    # 引数を取得
    a = sys.argv[1]
    b = sys.argv[2]

    # Calculatorインスタンスを作成
    calculator = Calculator(int(a), int(b))

    # 四則演算の結果をそれぞれ出力
    calculator.print()

説明

シンプルなプログラムです。Calculatorクラスに数値を与えたら、あとはインスタンスに「計算(処理)」と「出力」を任せます。

突然の仕様変更。バージョン2

このプロダクトについて、「出力結果をjson形式で保存したい」という要望があがりました。そのためソースを改修します。

出力はCalculatorクラスに書いているので、これを直しましょう。

ソース

calculator.py
import json
from typing import Dict


class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        # print(f'add: {self.a + self.b}')
        # print(f'subtract: {self.a - self.b}')
        # print(f'multiply: {self.a * self.b}')
        # print(f'divide: {self.a / self.b}')
        result: Dict[str, int] = {
            "add": self.a + self.b,
            "subtract": self.a - self.b,
            "multiply": self.a * self.b,
            "divide": self.a / self.b
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

実行結果

念のため、実行するとresult.jsonに次のようなテキストが出力されます(フォーマット済)。

result.json
{
   "add":10,
   "subtract":6,
   "multiply":16,
   "divide":4.0
}

リファクタリング

Calculatorクラスは四則演算の処理と、結果の出力を行っています。

これらを分けたほうが良いと判断して、出力処理を担当するPrinterクラスを作ることにしました。

.
└── koboridip
    ├── calculator.py
    ├── main.py
    └── printer.py
printer.py
import json
from typing import Dict


class Printer():
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))
calculator.py
from koboridip.printer import Printer


class Calculator():
    def __init__(self, a: int, b: int) -> None:
        self.a = a
        self.b = b

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        printer = Printer()
        printer.print(add, subtract, multiply, divide)

イヤな予感。バージョン3

その後の方針転換で、「CLIへの結果出力とjson形式の保存をどちらも利用したい」という判断がくだされました。次のようにしてモードを切り替えます。

$ python -m koboridip.main 8 2 simple
> (CLIに出力)

$ python -m koboridip.main 8 2 json
> (result.jsonを出力)

そのためPrinterクラスを2種類に分割して、切り替えられるようにしました。

プロジェクト構成

.
└── koboridip
    ├── calculator.py
    ├── json_printer.py -> json形式で出力
    ├── main.py
    ├── simple_printer.py -> CLIに出力

ソース

simple_printer.py
class SimplePrinter():
    def print(self, add, subtract, multiply, divide) -> None:
        print(f'add: {add}')
        print(f'subtract: {subtract}')
        print(f'multiply: {multiply}')
        print(f'divide: {divide}')

json_printer.py
import json
from typing import Dict


class JsonPrinter():
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

どちらで出力するかの判断はcalculator.pyに任せます。

指定される"simple"もしくは"json"という文字列を、mode変数に格納することで切り替えられるようにします。

calculator.py
from koboridip.simple_printer import SimplePrinter
from koboridip.json_printer import JsonPrinter


class Calculator():
    def __init__(self, a: int, b: int, mode: str) -> None:
        self.a = a
        self.b = b
        self.mode = mode

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        # 出力方法を切り替える
        if self.mode == 'json':
            json_printer = JsonPrinter()
            json_printer.print(add, subtract, multiply, divide)
        elif self.mode == 'simple':
            simple_printer = SimplePrinter()
            simple_printer.print(add, subtract, multiply, divide)

引数を取得できるようにmain.pyも変更しましょう。

main.py
import sys
from koboridip.calculator import Calculator

if __name__ == '__main__':
    # 引数を取得
    a = sys.argv[1]
    b = sys.argv[2]
    # 出力方式
    mode = sys.argv[3]

    # Calculatorインスタンスを作成
    calculator = Calculator(int(a), int(b), mode)

    # 四則演算の結果をそれぞれ出力
    calculator.print()

【重要】プロダクトの問題点

いまどうなっているのか

現状は、四則演算**"処理"Calculatorクラスが結果"出力"のPrinterクラスをimportしています。

この状態を、

Calculator(処理)はPrinter(出力)に依存している」

と表現します。

「依存している」とは

依存(import)が意味するのは、依存先の変更によって依存元も変更が必要になるという点です。

バージョン3で見たように、本プロジェクトは出力方式の追加(変更)をするためにCalculatorクラスも修正されました。

出力を変えたいだけなのに、処理も変えないといけなくなったのです。

仮に今後、「csv形式で出力したい」、「どこかのサーバに結果を飛ばしたい」といったような要望が増えたとしましょう。

そのたびにPrinterクラスはもとより、Calculatorクラスもなんらかの変更を余儀なくされます。

繰り返しになりますが、「処理(四則演算)」については一切の仕様変更が無いにも関わらず、処理機能の修正が必要になる。

ここに「違和感」を感じるところが重要です。

適切な依存関係をつくる

ここまで来ると「じゃあ依存先の変更に影響されないよう、依存を減らせばいいのか」という結論が出てくるかもしれません。

しかしPythonのプロジェクトでimportを使わないことはできないため、必ず依存は存在します。

つまり我々に必要な工夫は、「適切な依存関係」をつくることなのです。

それは**「変更の少ないほうに依存していること」**を指します。

補足(飛ばしてもOK)

このプロジェクトの問題点はもうひとつ、Calculatorが出力の詳細について知っていること、が挙げられます。

あくまでCalculatorは「結果を出力する」ことができればよくて、それがCLIだろうがjson形式だろうが、これを気にすることは避けたい、という目的もあります。

依存、そして逆転。バージョン4

それでは依存関係を逆転させましょう。

calculator.pyに抽象クラスであるPrinterクラスを置き、必要なABCMetaabstractmethodもインポートします。

calculator.py
from abc import ABCMeta, abstractmethod
from koboridip.simple_printer import SimplePrinter
from koboridip.json_printer import JsonPrinter


class Printer(metaclass=ABCMeta):
    @abstractmethod
    def print(self, add, subtract, multiply, divide):
        pass


class Calculator():
    def __init__(self, a: int, b: int, mode: str) -> None:
        self.a = a
        self.b = b
        self.mode = mode

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b

        # 出力方法を切り替える
        if self.mode == 'json':
            json_printer = JsonPrinter()
            json_printer.print(add, subtract, multiply, divide)
        elif self.mode == 'simple':
            simple_printer = SimplePrinter()
            simple_printer.print(add, subtract, multiply, divide)

そしてSimplePrinterJsonPrinterそれぞれを、Printerクラスを継承するように変更します。

simple_printer.py
from koboridip.calculator import Printer


class SimplePrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        print(f'add: {add}')
        print(f'subtract: {subtract}')
        print(f'multiply: {multiply}')
        print(f'divide: {divide}')
json_printer.py
import json
from typing import Dict
from koboridip.calculator import Printer


class JsonPrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        result: Dict[str, int] = {
            "add": add,
            "subtract": subtract,
            "multiply": multiply,
            "divide": divide
        }
        with open('result.json', mode='w') as f:
            f.write(json.dumps(result))

ここで重要なのはSimplePrinterたちがcalculator.pyに依存していることです。

ここで依存関係が逆転しました。"出力"が"処理"に依存しています。

当然まだ完璧ではありませんので、CalculatorクラスがSimplePrinterクラスに依存している状態を取り除きます。

そのために、コンストラクタでどちらのPrinterを利用するかを決めさせるようにしましょう。

calculator.py
from abc import ABCMeta, abstractmethod


class Printer(metaclass=ABCMeta):
    @abstractmethod
    def print(self, add, subtract, multiply, divide):
        pass


class Calculator():
    def __init__(self, a: int, b: int, printer:Printer) -> None:
        self.a = a
        self.b = b
        self.printer = printer

    def print(self) -> None:
        add = self.a + self.b
        subtract = self.a - self.b
        multiply = self.a * self.b
        divide = self.a / self.b
        self.printer.print(add, subtract, multiply, divide)

そしてmain.pyでどちらのPrinterを使うか指定させます。

main.py
import sys
from koboridip.calculator import Calculator, Printer
from koboridip.json_printer import JsonPrinter
from koboridip.simple_printer import SimplePrinter

if __name__ == '__main__':
    # 引数を取得
    a = sys.argv[1]
    b = sys.argv[2]
    # 出力方式
    mode = sys.argv[3]

    # Printerクラスを指定("simple"を判定するのは面倒なのでelseにしてしまいました)
    printer: Printer = JsonPrinter() if mode == 'json' else SimplePrinter()

    # Calculatorインスタンスを作成
    calculator = Calculator(int(a), int(b), printer)

    # 四則演算の結果をそれぞれ出力
    calculator.print()

calculate.pyにはimportがなく、代わりにsimple_printer.pyたちにimportがあります。

これで依存関係逆転が完成しました。

エピローグ。バージョン5

さきほど想定した通り、csv形式での出力も要望されました。

これまでは出力方式に変更があるたびにCalculatorクラスも影響を受けていましたが、これがどうなるかを確認してみましょう。

.
└── koboridip
    ├── calculator.py
    ├── csv_printer.py
    ├── json_printer.py
    ├── main.py
    └── simple_printer.py
csv_printer.py
import csv
from typing import List
from koboridip.calculator import Printer


class CsvPrinter(Printer):
    def print(self, add, subtract, multiply, divide) -> None:
        result: List[List] = []
        result.append(["add", add])
        result.append(["subtract", subtract])
        result.append(["multiply", multiply])
        result.append(["divide", divide])

        with open('result.csv', 'w') as f:
            writer = csv.writer(f)
            writer.writerows(result)
main.py
import sys
from koboridip.calculator import Calculator, Printer
from koboridip.json_printer import JsonPrinter
from koboridip.simple_printer import SimplePrinter
from koboridip.csv_printer import CsvPrinter


if __name__ == '__main__':
    # 引数を取得
    a = sys.argv[1]
    b = sys.argv[2]
    # 出力方式
    mode = sys.argv[3]

    # Printerクラスを指定
    printer: Printer = JsonPrinter() if mode == 'json' else CsvPrinter(
    ) if mode == 'csv' else SimplePrinter()

    # Calculatorインスタンスを作成
    calculator = Calculator(int(a), int(b), printer)

    # 四則演算の結果をそれぞれ出力
    calculator.print()

こうすることでcsvファイルの出力もできました。

以降も簡単に出力方式を変更できることが想像できるかと思います。

さいごに

依存関係逆転の原則を理解する手助けになれば幸いです。最後に1点補足を。

これまでのバージョンは誤りだったのか

「依存関係逆転の原則を理解した!」となった人は、つぎに適切な依存関係でなさそうなプロジェクトを見ると「これは問題だ!」と設計・実装を即座に直そうとします(自分がそうです)。

たとえばバージョン2のリファクタリング後でCalculatorクラスがPrinterクラスに依存しているので、この時点で依存関係逆転の原則を適用したくなるはずです。

しかしこれは時期尚早です。もちろんこのタイミングで「出力方式はいくらでも増えることがある」と分かっていれば適用すべきですが、一方で「出力方式が変更されることはあまりなさそう」であれば、適用を**《保留》**することも良い判断になりえると思います。

個人的には出力のような「詳細」は早めに依存関係を整理しておきたくなりますが、すくなくとも「いつでも変更はできるよなー」と思っておけることが重要なのかなと考えています。

依存性の注入(インジェクション)

時間があれば、この題材のまま「DI=依存性の注入」についても書ければと思います。

指摘や質問などあれば、ぜひコメントいただければ嬉しいです。

20
13
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?