「依存関係逆転の原則」についてのテキストは数多くありますが、
- 理由がよくわからない
- やり方がよくわからない
という人向けに、自分なりの「こう説明してくれればわかりやすかった」という記事を書いてみます。
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
ソース
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}')
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クラスに書いているので、これを直しましょう。
ソース
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
に次のようなテキストが出力されます(フォーマット済)。
{
"add":10,
"subtract":6,
"multiply":16,
"divide":4.0
}
リファクタリング
Calculator
クラスは四則演算の処理と、結果の出力を行っています。
これらを分けたほうが良いと判断して、出力処理を担当するPrinter
クラスを作ることにしました。
.
└── koboridip
├── calculator.py
├── main.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))
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に出力
ソース
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}')
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
変数に格納することで切り替えられるようにします。
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
も変更しましょう。
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
クラスを置き、必要なABCMeta
、abstractmethod
もインポートします。
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)
そしてSimplePrinter
、JsonPrinter
それぞれを、Printer
クラスを継承するように変更します。
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}')
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を利用するかを決めさせるようにしましょう。
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を使うか指定させます。
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
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)
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=依存性の注入」についても書ければと思います。
指摘や質問などあれば、ぜひコメントいただければ嬉しいです。