pipの呼び出し部分の設計を調べてみた
main関数からどのように機能を呼び出すべきなのか考えています。
参考になりそうな、有名で、かつそんなに難しくなさそうなツールはないかな?と考えていたら思いついたのがpipです。
おそらくPythonで最も有名で、そこそこ高機能だけど、そんなに複雑でなさそうなCLIツール。ぴったりの題材だと思って調べてみました。
ソースコード入手
GitHubにありました。
GitHub - pypa/pip: The Python package installer
エントリーポイントの確認
setup.pyを確認します。
entry_points={ "console_scripts": [ "pip=pip._internal.cli.main:main",
pip._internal.cli.main.pyのmain関数がエントリーポイントだとわかりました。
シーケンス図
エントリーポイントから各機能への呼出しを追いかけて図にしました。
ここでは「pip install」を例として取り上げていますが他の機能も同じ流れです。
正確なUML表記ではありません。関連性が低いMixin関連の親クラスは省略してます。
クラス図
クラス図も作りました。
Commandパターンっぽい
なんとなく予想してましたがCommandパターンっぽいです。
mainがInvoker役、CommandはそのままCommand役、commandsパッケージの各クラスはConcreteCommand兼Receiver役です。
一般的なCommandパターンと異なりConcreteCommandとReceiverが分離していません。ConcreteCommandにはundoやredoがなくrunのみのシンプルな実装でよいので処理詳細をReceiverに移譲するのは過剰だと判断したのかもしれません。もしくはPythonの流儀としてはこれくらいの処理はクラスをExtractせずにやるものなのかもしれません。各機能の見通しはいいと思います。
Template Methodパターンっぽい
最初は気づきませんでしたが、図にして初めて気づきました。CommandクラスとConcreteCommandクラスがTemplate Methodパターンになっています。
Commandクラスの_mainメソッドは以下のようになっています。
def _main(self, args): <環境変数の確認やvirtualenvかどうかのチェックなど> status = self.run(options, args) <Exception処理など>
そしてrunメソッドの実装は以下の通り子クラスに任せています。
def run(self, options, args): # type: (Values, List[Any]) -> int raise NotImplementedError
このようにTemplate Methodパターンで機能実行前の環境チェックや実行時に発生したエラー処理などの共通処理を共通化しつつ、機能自体は各ConcreteCommandクラスに委ねています。
routing機能
「list」や「install」などの第1引数を元にどういうルーティングをしているのか気になっていましたが意外にシンプルでした。
pip._internal.commands.__init__.pyに第1引数と対応するConcreteCommandクラス名のテーブルがあります。OrderedDict形式です。
mainは第1引数を元に__init__.pyに問い合わせます。__init__.pyは第1引数に応じたクラス名を取得し、import_moduleやgetattrを使ってインスタンスを作成してmainに返します。mainは取得したインスタンスがなにか意識せずにそのmainメソッドを呼び出します。
循環依存?
cliパッケージとcommandsパッケージの間に循環依存があるように見えます。クラス図 左側の矢印はcliパッケージからcommandsパッケージへ向かい、右側の矢印は逆に向かっています。
私のクラス図の書き方が悪いのかもしれません。
書き方が間違っていない場合、どうして循環しているのでしょう?1つの仮説を立ててみました。Duck Typingです。
Javaならクラス図 左側のmainと__init__の間にInterfaceを作って依存関係を逆転させるところでしょう。Depencency Inversion Principleです。
でもPythonは動的型付け言語なのでInterfaceを明示的に作成する必要がありません。つまりmainと__init__の間にはすでに暗黙のInterfaceが存在し、そのInterfaceには抽象メソッドcreate_commandのみが定義されているのです。
mainは暗黙のうちにこの見えないInterfaceを介して__init__を利用している、いわゆるDuck Typingです。見えないけどすでに依存関係は逆転しているので循環依存は存在しない、、、のかな?自信ありません。