はじめに
ドドスコスコスコ nikkieです。
先日injectorというPythonライブラリを触りました。
ドキュメントの例から離れて、少しだけ難易度を上げた素振りをしてみました。
目次
依存性注入して作るFizzBuzzアプリ
ちょうぜつ本(『ちょうぜつソフトウェア設計入門』)7章にあります。
PHPのSymfonyのServiceContainerを使ってDIしていきます。
例は(5〜6章で作ってきた)FizzBuzzのアプリケーションです。
こんな感じでオートワイヤリングされます
- FizzBuzzSequencePrinter(アプリケーション本体)
これをPythonで、injectorライブラリを使って再実装します。
動作環境
- Python 3.11.4
- injector 0.21.0
injectorによる再実装
ソースコードの全容は以下にあります
injectorのやり方に合わせて、以下のように変えました
- FizzBuzzSequencePrinter: inject
- NumberConverter: inject
- いくつかの ReplaceRuleInterface インスタンスが渡る: Moduleに定義
- 個々のRuleはinject
- いくつかの ReplaceRuleInterface インスタンスが渡る: Moduleに定義
- OutputInterface: abcとした上で、bindを定義
- ConsoleOutput
- NumberConverter: inject
inject!(イニシャライザをデコレート)
イニシャライザを@inject
でデコレートして、オートワイヤリングされる対象にします
class FizzBuzzSequencePrinter: @inject def __init__( self, fizzbuzz: NumberConverter, output: OutputInterface ) -> None:
class NumberConverter: @inject def __init__(self, rules: Iterable[ReplaceRuleInterface]) -> None:
NumberConverter
FizzBuzzのロジッククラスへの依存性注入ですが、provider(multiprovider)という仕組みを使いました1。
https://injector.readthedocs.io/en/latest/api.html#injector.multiprovider
DIコンテナはIterable[ReplaceRuleInterface]
として、3の倍数・5の倍数・何もしないルールの3つに依存するように定義します。
Module2を継承したクラスに@multiprovider
でデコレートしたメソッドを実装!
なお、このメソッドも依存を注入しています(徹底!)。
class FizzRule(CyclicNumberRule): def __init__(self) -> None: super().__init__(3, "Fizz") class BuzzRule(CyclicNumberRule): def __init__(self) -> None: super().__init__(5, "Buzz") class FizzBuzzAppModule(Module): @multiprovider def provide_output_interface( self, fizz_rule: FizzRule, buzz_rule: BuzzRule, pass_rule: PassThroughRule, ) -> Iterable[ReplaceRuleInterface]: return [fizz_rule, buzz_rule, pass_rule]
DIコンテナにgetすると、「Iterable[ReplaceRuleInterface]
型の依存にはprovide_output_interface
メソッドの返り値!」と依存が注入されるわけですね。
OutputInterface
Protocolがサポートされていなさそうだったので、抽象クラス(abc)を使った実装に切り換えました。
OutputInterface
のインスタンスとしてはConsoleOutput
インスタンスを返すように、DIコンテナに設定3。
これがFizzBuzzSequencePrinter
のOutputInterface
への依存に注入されます。
def configure_output(binder): binder.bind(OutputInterface, to=ConsoleOutput())
依存がつながるように抽象-具象の対応を定義していくと理解しました。
DIコンテナの設定
injector = Injector([FizzBuzzAppModule, configure_output])
独自で定義したFizzBuzzAppModule
クラスやconfigure_output
関数も初期化時に渡します。
これによりイニシャライザの型ヒントだけでなく、独自に定義したproviderやbindもDIコンテナが見てくれるようになるという理解です4。
こぼれ話:NumberConverterの方はbindがうまくいかない
NumberConverterはbinder.bindの実装ではうまくいかず、2つのやり方に違いがあることを体感しました
def configure_rules(binder): rules = [ CyclicNumberRule(3, "Fizz"), CyclicNumberRule(5, "Buzz"), PassThroughRule(), ] binder.bind(Iterable[ReplaceRuleInterface], to=rules)
「Iterable[ReplaceRuleInterface]
型にはこの特定のインスタンス」とDIコンテナに指定するには、multiproviderになるようです。
終わりに
ちょうぜつ本のオートワイヤリングの例をPythonでinjectorを使って再実装しました。
injectorは型ヒントを使って依存を数珠つなぎにしているように見えています。
ドキュメントの例から離れるとエラーの解消がなかなか大変でしたが、「こうやったら関数が型で数珠つなぎになるんじゃないか」と組み立てていくのは、ドキュメントの例の写経とは違った学びがありました!
injectorは触り始めたばかりで理解できているとはまだまだ言えません。
ここで書いた書き方よりよい書き方など、お気づきの点があれば教えていただけると嬉しいです
-
最初に
provider
を試したのですが、Iterable[ReplaceRuleInterface]
という型にはmultiprovider
と案内されました↩ - https://injector.readthedocs.io/en/latest/api.html#injector.Module↩
- bind https://injector.readthedocs.io/en/latest/api.html#injector.Binder.bind↩
- modules引数を指定しています。ref: https://injector.readthedocs.io/en/latest/api.html#injector.Injector↩