nikkie-ftnextの日記

イベントレポートや読書メモを発信

#ちょうぜつ本 7章の依存性注入 FizzBuzzアプリのオートワイヤリングについて、Pythonでinjectorを使った再実装を試みる💉

はじめに

ドドスコスコスコ nikkieです。

先日injectorというPythonライブラリを触りました。

ドキュメントの例から離れて、少しだけ難易度を上げた素振りをしてみました。

目次

依存性注入して作るFizzBuzzアプリ

ちょうぜつ本(『ちょうぜつソフトウェア設計入門』)7章にあります。
PHPSymfonyのServiceContainerを使ってDIしていきます。
例は(5〜6章で作ってきた)FizzBuzzのアプリケーションです。

こんな感じでオートワイヤリングされます

  • FizzBuzzSequencePrinter(アプリケーション本体)
    • NumberConverter(FizzBuzzロジック)
      • いくつかの ReplaceRuleInterface インスタンスが渡る
        • 3の倍数でFizzを表す CyclicNumberRule
        • 5の倍数でBuzzを表す CyclicNumberRule
        • 何もしないルール PassThroughRule
    • OutputInterface
      • ConsoleOutput

これをPythonで、injectorライブラリを使って再実装します。

動作環境

  • Python 3.11.4
  • injector 0.21.0

injectorによる再実装

ソースコードの全容は以下にあります

injectorのやり方に合わせて、以下のように変えました

  • FizzBuzzSequencePrinter: inject
    • NumberConverter: inject
      • いくつかの ReplaceRuleInterface インスタンスが渡る: Moduleに定義
        • 個々のRuleはinject
    • OutputInterface: abcとした上で、bindを定義
      • ConsoleOutput

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
これがFizzBuzzSequencePrinterOutputInterfaceへの依存に注入されます。

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は触り始めたばかりで理解できているとはまだまだ言えません。
ここで書いた書き方よりよい書き方など、お気づきの点があれば教えていただけると嬉しいです


  1. 最初にproviderを試したのですが、Iterable[ReplaceRuleInterface]という型にはmultiproviderと案内されました
  2. https://injector.readthedocs.io/en/latest/api.html#injector.Module
  3. bind https://injector.readthedocs.io/en/latest/api.html#injector.Binder.bind
  4. modules引数を指定しています。ref: https://injector.readthedocs.io/en/latest/api.html#injector.Injector