Slide 1

Slide 1 text

Python で Dependency Injection(DI) をやるには? 2019/2/23 @shimakaze_soft shimakaze-soft 1

Slide 2

Slide 2 text

Who are you? おまえだれよ? from dataclasses import dataclass @dataclass class User: """""" name: str = "shimakaze_soft" shimakaze-soft 2

Slide 3

Slide 3 text

Who are you? おまえだれよ? Twitter: @shimakaze_soft GitHub: shimakaze-git shimakaze-soft 3

Slide 4

Slide 4 text

皆さん、DI(Dependency Injection)ってご存知です か? shimakaze-soft 4

Slide 5

Slide 5 text

依存性の注入とは? 依存性の注入(いそんせいのちゅうにゅう、英: Dependency injection)とは、コンポーネント間の依存関係をプログラムのソース コードから排除し、外部の設定ファイルなどで注入できるようにするソフ トウェアパターンである。 依存性の注入 / Wikipedia shimakaze-soft 5

Slide 6

Slide 6 text

どゆこと...? shimakaze-soft 6

Slide 7

Slide 7 text

shimakaze-soft 7

Slide 8

Slide 8 text

shimakaze-soft 8

Slide 9

Slide 9 text

依存性の注入(DI)って? そもそも「 依存性 の注入」の「 依存性(Dependency) 」という 言葉が惑わせてる Dependency とはオブジェクトのこと 「オブジェクトの注入」と考えたほうがいい shimakaze-soft 9

Slide 10

Slide 10 text

つまりはDependency Injectionって? あるオブジェクト(サービス)を別のオブジェクト(クライア ント)に渡すパターンのこと あるコンポーネントが特定のコンポーネントに依存しなくなり、 コードの変更がしやすくなる shimakaze-soft 10

Slide 11

Slide 11 text

shimakaze-soft 11

Slide 12

Slide 12 text

実例を用いた方がわかりやすい shimakaze-soft 12

Slide 13

Slide 13 text

以下のような簡単なものを作成してみる コンストラクタでタイトルと説明文を受け取るクラス タイトルと説明文を入れたJSONファイルを生成する shimakaze-soft 13

Slide 14

Slide 14 text

実際に作成してみる - 1 import json # Jsonファイルを生成するクラス class JsonOutputFile: title: str description: str def output(self, title: str, description: str) -> None: self.title = title self.description = description output: dict[str, str] = { "title": self.title, "description": self.description } with open("output.json", "w") as f: json.dump(output, f, indent=4) shimakaze-soft 14

Slide 15

Slide 15 text

実際に作成してみる - 2 class Report: title: str description: str output_obj: JsonOutputFile def __init__(self, title: str, description: str) -> None: self.title = title self.description = description self.output_obj = JsonOutputFile() def output_file(self) -> None: self.output_obj.output(self.title, self.description) title: str = "title" description: str = "description" report: Report = Report(title, description) report.output_file() shimakaze-soft 15

Slide 16

Slide 16 text

直接インスタンスを生成しているので、変更に少し弱 い実装 shimakaze-soft 16

Slide 17

Slide 17 text

直接インスタンスを生成しているので、変更に少し弱 い実装 class Report: title: str description: str output_obj: JsonOutputFile def __init__(self, title: str, description: str) -> None: self.title = title self.description = description # この行でファイルを書き込むクラスのJsonOutputFileのインスタンスを直接生成している self.output_obj = JsonOutputFile() def output_file(self) -> None: self.output_obj.output(self.title, self.description) shimakaze-soft 17

Slide 18

Slide 18 text

以下のように作り変えなければいけない場合は? 先程は「タイトルと説明文を入れたJSONファイルを生成する」という仕様 「タイトルと説明文を入れたHTMLファイルを生成する」という仕 様に作り変えなければいけない場合は? shimakaze-soft 18

Slide 19

Slide 19 text

実際に作成してみる - 1 class HtmlOutputFile: def output(self, title: str, description: str) -> None: text: str = "\n" text += "\n" text += "" + title + "\n" text += "\n" text += "\n" text += "

" + description + "

\n" text += "\n" with open("output.html", mode="w") as f: f.write(text) shimakaze-soft 19

Slide 20

Slide 20 text

実際に作成してみる - 2 インスタンス変数である output_obj に対して、 JsonOutputFile から HtmlOutputFile に直接書き換えなければいけない。 class Report: title: str description: str output_obj: HtmlOutputFile def __init__(self, title: str, description: str) -> None: self.title = title self.description = description # この行で直接、HtmlOutputFileに書き換えている self.output_obj = HtmlOutputFile() def output_file(self) -> None: self.output_obj.output(self.title, self.description) shimakaze-soft 20

Slide 21

Slide 21 text

先程のコードの問題点 クラスのインスタンスを直接生成してしまっている 「JSONファイルの生成からHTMLファイルの生成に変更」 などの場合のように、変更に弱くな る実装 Reportクラス が JsonOutputFileクラス または HtmlOutputFileクラス に依存してい る状態 class Report: .... def __init__(self, title: str, description: str) -> None: .... # この行で直接、JsonOutputFileからHtmlOutputFileに書き換えなければいけない # self.output_obj = JsonOutputFile() self.output_obj = HtmlOutputFile() shimakaze-soft 21

Slide 22

Slide 22 text

じゃあどうする? shimakaze-soft 22

Slide 23

Slide 23 text

実際にDIをやってみる shimakaze-soft 23

Slide 24

Slide 24 text

DIを適用する例 - 1 インスタンスをコンストラクタで受け取る インスタンスの生成は外側で行う shimakaze-soft 24

Slide 25

Slide 25 text

DIを適用する例 - 2 class Report: title: str description: str def __init__(self, title: str, description: str, output_obj) -> None: self.title = title self.description = description # インスタンスをコンストラクタで受け取る self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) title: str = "title" description: str = "description" # インスタンスの生成は外側で行う output_obj: HtmlOutputFile = HtmlOutputFile() # output_obj: JsonOutputFile = JsonOutputFile() report: Report = Report(title, description, output_obj) report.output_file() shimakaze-soft 25

Slide 26

Slide 26 text

これで、めでたし? shimakaze-soft 26

Slide 27

Slide 27 text

先程の実装の問題点 Reportクラスのコンストラクタの引数 output_obj がどんなオブジェクトでも受け 取れる 動的型付け言語であるPythonには引数に 型指定 などが無いため、実行時にもエラ ーにはならない shimakaze-soft 27

Slide 28

Slide 28 text

先程の実装の問題点 class Report: title: str description: str # コンストラクタの引数、output_objの型が不明 def __init__(self, title: str, description: str, output_obj) -> None: self.title = title self.description = description # インスタンス変数であるoutput_objの型が不明 self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) title: str = "title" description: str = "description" output_obj: HtmlOutputFile = HtmlOutputFile() # output_obj: JsonOutputFile = JsonOutputFile() report: Report = Report(title, description, output_obj) report.output_file() shimakaze-soft 28

Slide 29

Slide 29 text

ある程度の制約は設ける コンストラクタの引数である output_obj が JsonOutputFile であることがわかる class Report: title: str description: str output_obj: JsonOutputFile # output_objがJsonOutputFileのインスタンス def __init__(self, title: str, description: str, output_obj: JsonOutputFile) -> None: self.title = title self.description = description self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) shimakaze-soft 29

Slide 30

Slide 30 text

Pythonにも型指定は一応ある Python 3.5から導入された Type Hints(型ヒント) という機能 引数で受け取る型を指定できる shimakaze-soft 30

Slide 31

Slide 31 text

特定のクラスの型しか受け取れない この例では JsonOutputFile しか受け取れない class Report: title: str description: str output_obj: JsonOutputFile def __init__(self, title: str, description: str, output_obj: JsonOutputFile) -> None: self.title = title self.description = description self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) shimakaze-soft 31

Slide 32

Slide 32 text

特定のクラスに依存してしまっている Report が JsonOutputFile に依存している => HtmlOutputFile を引数に指定 できない shimakaze-soft 32

Slide 33

Slide 33 text

じゃあどうする? shimakaze-soft 33

Slide 34

Slide 34 text

抽象に依存させる shimakaze-soft 34

Slide 35

Slide 35 text

抽象に依存させるとは? 具体的な処理を記述した具象に依存するのではなく、抽象に依存する 何を言っている? shimakaze-soft 35

Slide 36

Slide 36 text

JsonOutputFileとHtmlOutputFileのどちらもoutputというメソッ ドを実装していた class JsonOutputFile: title: str description: str def output(self, title: str, description: str) -> None: self.title = title self.description = description output: dict[str, str] = { "title": self.title, "description": self.description } with open("output.json", "w") as f: json.dump(output, f, indent=4) shimakaze-soft 36

Slide 37

Slide 37 text

JsonOutputFileとHtmlOutputFileのどちらもoutputというメソッ ドを実装していた class HtmlOutputFile: def output(self, title: str, description: str) -> None: text: str = "\n" text += "\n" text += "" + title + "\n" text += "\n" text += "\n" text += "

" + description + "

\n" text += "\n" with open("output.html", mode="w") as f: f.write(text) shimakaze-soft 37

Slide 38

Slide 38 text

抽象に依存させるとは? - 2 outputメソッドを必ず実装したクラスを引数で受け取れるようにする インターフェースを持ったクラスを引数で受け取れるようにする shimakaze-soft 38

Slide 39

Slide 39 text

インターフェースとは 具体的な処理は書けない メソッド名と引数は記述できる インターフェースを使用したクラスはメソッドの実装を強制させる 特定のメソッドと受け取れる引数が実装されることを保証させる もの shimakaze-soft 39

Slide 40

Slide 40 text

インターフェースとは - 2 具体的な処理は書けない メソッド名と引数は記述できる インターフェースを使用したクラスはメソッドの実装を強制させる interface AbstractOutputFile: def output(self, title: str, description: str) -> None: pass shimakaze-soft 40

Slide 41

Slide 41 text

インターフェースという機能・・・ shimakaze-soft 41

Slide 42

Slide 42 text

PythonにはInterfaceは備えていない 以下は実際には無い構文 interface AbstractOutputFile: def output(self, title: str, description: str) -> None: pass shimakaze-soft 42

Slide 43

Slide 43 text

じゃあどうするか? shimakaze-soft 43

Slide 44

Slide 44 text

抽象クラス(abc)を使う shimakaze-soft 44

Slide 45

Slide 45 text

Abstract Base Class(abc)とは - 1 抽象クラス 実際の処理を自身にではなく子クラスに記述させるための抽象メソッドを定義し たクラス 抽象クラスを継承したクラスは抽象メソッドをオーバーライドして処理を実装し なければいけない 抽象クラス単体ではインスタンスの生成が不可能 shimakaze-soft 45

Slide 46

Slide 46 text

Abstract Base Class(abc)とは - 2 抽象クラスはインターフェース同様にメソッドの実装を強制させる shimakaze-soft 46

Slide 47

Slide 47 text

PythonのAbstract Base Class(abc) Pythonには abc という抽象クラスを実現するモジュールを標準で備えている from abc import ABC, abstractmethod class AbstractOutputFile(ABC): @abstractmethod def output(self, title: str, description: str) -> None: raise NotImplementedError() shimakaze-soft 47

Slide 48

Slide 48 text

抽象に依存させたコード 抽象クラスである AbstractOutputFile from abc import ABC, abstractmethod class AbstractOutputFile(ABC): @abstractmethod def output(self, title: str, description: str) -> None: raise NotImplementedError() shimakaze-soft 48

Slide 49

Slide 49 text

抽象に依存させたコード - 2 AbstractOutputFile を JsonOutputFile と HtmlOutputFile に継承させる shimakaze-soft 49

Slide 50

Slide 50 text

抽象に依存させたコード - 3 `AbstractOutputFile`を継承 class JsonOutputFile(AbstractOutputFile): title: str description: str def output(self, title: str, description: str) -> None: self.title = title self.description = description output: dict[str, str] = { "title": self.title, "description": self.description } with open("output.json", "w") as f: json.dump(output, f, indent=4) # `AbstractOutputFile`を継承 class HtmlOutputFile(AbstractOutputFile): def output(self, title: str, description: str) -> None: text: str = "\n" text += "\n" text += "" + title + "\n" text += "\n" text += "\n" text += "

" + description + "

\n" text += "\n" with open("output.html", mode="w") as f: f.write(text) shimakaze-soft 50

Slide 51

Slide 51 text

抽象に依存させたコード - 3 AbstractOutputFile型 を引数で受け取れるようにする class Report: title: str description: str # インスタンス変数であるoubput_objはAbstractOutputFile型 output_obj: AbstractOutputFile # コンストラクタの引数であるoubput_objはAbstractOutputFile型 def __init__(self, title: str, description: str, output_obj: AbstractOutputFile) -> None: self.title = title self.description = description self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) shimakaze-soft 51

Slide 52

Slide 52 text

依存性逆転の原則(DIP) 元々は、以下のように具象クラスに直接依存していた関係 shimakaze-soft 52

Slide 53

Slide 53 text

依存性逆転の原則(DIP) 抽象クラスに依存することで、具象クラスである HtmlOutputfile と JsonOutputFile は依存される側から、依存する側になった(-> AbstractOutputFile) これが依存性逆転の原則(Dependency Inversion Principle) shimakaze-soft 53

Slide 54

Slide 54 text

ようやくめでたし? shimakaze-soft 54

Slide 55

Slide 55 text

実はもう一つ注意点・・・ shimakaze-soft 55

Slide 56

Slide 56 text

実は継承しなくても動きます outputメソッド を実装しているインスタンスであれば動いてしまう # AbstractOutputFileを継承していない class OtherOutputFile: def output(self, title: str, description: str) -> None: """""" .... shimakaze-soft 56

Slide 57

Slide 57 text

実は継承しなくても動きます # AbstractOutputFileを継承していない class OtherOutputFile: def output(self, title: str, description: str) -> None: """""" .... class Report: title: str description: str output_obj: AbstractOutputFile def __init__(self, title: str, description: str, output_obj: AbstractOutputFile) -> None: self.title = title self.description = description self.output_obj = output_obj def output_file(self) -> None: self.output_obj.output(self.title, self.description) output_obj: OtherOutputFile = OtherOutputFile() report: Report = Report("title", "description", output_obj) report.output_file() shimakaze-soft 57

Slide 58

Slide 58 text

Pythonにも型指定は一応ある Python 3.5に導入されたType Hints(型ヒント)という機能 「引数で受け取る型を指定できる」とはいいつつも違う型を引数に入れても受け取っ てしまう shimakaze-soft 58

Slide 59

Slide 59 text

mypyでチェックする 正しい型の値 が入っているかをチェックできる mypy というサードパーティのツールが あるため、mypyでチェックを行う # インストール方法 $ pip install mypy $ mypy sample.py Success: no issues found in 1 source file Success: no issues found in 1 source file と出れば型定義に問題なく成功 shimakaze-soft 59

Slide 60

Slide 60 text

mypyでチェックする HtmlOutputFile から AbstractOutputFile の継承を外してみる class HtmlOutputFile(): def output(self, title: str, description: str) -> None: """""" .... 以下のようなエラーが出るはずです。 $ mypy sample.py sample.py:63: error: Argument 3 to "Report" has incompatible type "HtmlOutputFile"; expected "AbstractOutputFile" [arg-type] Found 1 error in 1 file (checked 1 source file) shimakaze-soft 60

Slide 61

Slide 61 text

まとめ PythonでDIPやる場合は抽象クラス(abc)を使う 型チェックの実装はmypyを使用する pyrightというのもあります 参考資料 PythonでのDependency Injection 依存性の注入 shimakaze-soft 61

Slide 62

Slide 62 text

ご清聴ありがとうございました! shimakaze-soft 62