作ったのはこんなもの
Python でちょっとした個人開発を行っていて、DIフレームワークInjectorを使っていました。
ただ、フレームワークのために度々ちょっとした実装を行わなければならず、それを何度か繰り返すうちに煩わしく感じてきました。
その辺りを解消するライブラリとかツール無いかな~と軽く調べてみたのですが無さそうだったので、そんじゃいっちょVSCode拡張自作してみっか、ということで開発したのがこちらになります。
DIとは
DI とは Dependency Injection の略で、日本語に訳すと 依存注入
だそうです。
これだけだと初見ではイマイチピンときませんね。
もう少し詳しく説明すると、
- ある処理(
A
とする)の中で外部で実装された別の処理(B
とする)を実行することをAはBに依存する
と言う - 依存の対象となるクラスオブジェクト、関数オブジェクトを引数として渡すことを
依存を注入する
と言う
となり、サンプルコードで表すと、
class SomeService:
def __init__(self, dependency: DependencyService):
self.dependency = dependency # 依存注入
def run(self):
self.dependency.exec() # 依存を使用
みたいな感じになります。
DIの何がいいのか
DIの最大の利点は、依存対象を差し替えやすくなる ことです。
例えば、実際の本番処理の代わりにテスト用のモックやスタブを注入すれば、外部サービスやデータベースに依存せず動作確認ができます。
また、依存を外部から与える設計にすると、クラスや関数自体が特定の実装に縛られなくなるため、
- 機能追加や仕様変更の際に影響範囲が小さい
- 実装の再利用が容易
- ユニットテストが書きやすい
といったメリットがあります。
これは疎結合な設計を促進し、長期的な保守性や拡張性の向上につながります。
DIコンテナについて
DIを活用するうえで重要なのが DIコンテナ
という概念です。
DIの仕組みをアプリ全体で活用しようとすると、クラスやサービス間の依存関係をまとめて管理する必要が出てきます。
例えば、「このクラスにはこの依存を渡す」という紐付けを毎回手書きで行うと、規模が大きくなるにつれて初期化コードが膨らみ、メンテナンスも大変になります。
# 直に依存注入を実装するイメージ
service = SomeService(Dependency1(Dependency2(), Dependency3(Dependency4())))
そこで役立つのが DIコンテナ
です。
DIコンテナは、
- どのクラスにどの依存を渡すか
- 必要なタイミングでインスタンスを生成する方法
を一括で管理してくれる仕組みです。
言い換えると、「依存関係の生成と注入をまとめて自動化する工場」のようなものです。
個々の言語やフレームワークによりますが、DIコンテナにクラス名を引数として指定し、依存注入されたインスタンスを受け取る
といった実装をされていることが多いようです。
# 依存注入済みのSomeServiceインスタンス
service = injector.get(SomeService)
Python の Injector について
さて、今回のお題とも関係の深い、Python の Injector
についてのお話です。
Gitリポジトリはこちら。
Injector には何通りかDIを実現するための実装法が用意されていますが、
上述したDIの利点をフルに活かすには、 Module
を継承、 @singleton, @provide
を付加したメソッドを実装した Module クラスを作成する方法をとることになります。
class SomeService:
def __init__(self, dependency: Dependency):
self.dependency
# SomeService に対する Module
class SomeServiceModule(Module):
@singleton
@provide
def provide(self, dependency: Dependency) -> SomeService:
return SomeService(dependency)
# Dependency の Module も必要
class DependencyModule(Module):
@singleton
@provide
def provide(self) -> Dependency:
return Dependency()
# Injector を初期化
injector = Injector([SomeServiceModule(), DependencyModule()])
...
# DI済みの SomeService
service = injector.get(SomeService)
ここでは分かりやすくするためまとめて記述しましたが、実際には個別のファイルにそれぞれのコードを記述することになります。
さて、ここで新しくDI対象クラスを実装した時の作業を想定してみましょう。
- 対象の Module ファイル、クラスの作成
- DIされるクラスに対する Module ファイル、クラスも(無ければ)作成
- Injector 初期化コードの更新
また、Module 作成済みのクラスのDIを修正することになった場合にも、
- Module の引数の更新
- 新しくDIされるクラスが追加されていたらそれに対する Module ファイル、クラスも追加
の作業が発生します。
一つ一つは大したことのない作業ですが、リファクタで大規模なロジックの整理を行った時などは、いくつものファイルの引数を順次修正していくという単調な作業が続くことになります。
また、新しく大規模なロジックを実装する場合も、作業中にロジック実装とは直接関係のない Module 実装を逐一挟むか、実装完了後に大量の Module 作成をまとめて行うことになり、なかなか馬鹿にならない時間を費やすことになります。後者の場合、Module 作成を見落としていて動作確認時にエラーということもあり得ます。
よし、拡張だ
ここで冒頭の話に戻ります。
これまでに述べたように、DIには様々な恩恵がありますが、Injectorでそれを享受しようとすると結構な手間がかかります。
そこで、その手間を可能な限り削ってしまえば、メリットだけを享受できるじゃないか、という考えに至ります。
今回開発した拡張機能では、右クリック一発でファイル移動もすることなく、前述の作業が完了するようになっています。
これで作業の手間は限りなくゼロに近づき、DIのメリットをほくほく顔で享受できるようになりました。
拡張機能の中身など
実装自体は何のことはありません。
- VSCodeのクリックイベントから対象ファイルのパスを取得
- ファイルを読み込んで構文解析(
ast
使用) - 解析結果から必要な情報(クラス名、コンストラクタ引数など)を取得
- 情報をテンプレートに当てはめてソースコード出力
という、挙動から推測できることをそのまま実行しているだけです。
あれば便利そうな機能なのにこれまで誰も開発しなかったのは、必要とする人は自前のツールで何とかしてたからかもしれませんね。
私の場合、VSCode拡張の開発や公開の経験を積んでみたかったのでこういう形で実現してみたわけですが。
そのうちもっと汎用性の高い機能を開発してみたいですね。