依存関係に立ち向かうための地図を作ろう


はじめに

今年作ったソースコードというかプロジェクトのレビューツールを紹介したいと思います。
個人的にちょっと面白い機能も付けられたと思うので、何かしら刺激になれば幸いです!

なお、せっかくなので「本番環境でやらかしちゃった人カレンダー」の人気にあやかりたく、「開発プチやらかしカレンダー」くらいのノリでやっていきます。

ツール紹介

まずは成果物から紹介してしまいます。
D言語のモジュール依存関係をグラフ表示するための ddeps というツールを作りました。

主な機能は「依存関係の可視化」ですが、ちょっと珍しい機能として「2つのバージョン間で依存関係の差分を表示する」ことができます。

ソースとドキュメントは以下にまとめてありますので興味があれば見てみてください。
(下の方にサンプルもあります)

果たして、一体これが何の役に立つんでしょうね???

開発経緯

日々の開発

そこそこな人数で、そこそこな規模の製品を開発していました。
モジュール数は数千を超え、全体で何行あるか数えたこともありませんが、とにかく全体が見渡せない程度には巨塔でした。

しかし先人たちはそうなることを見越してか、担当部署に合わせてレイヤーを分け、疎結合を意識した設計がなされていました。
おかげで日々の開発はそこまで苦しくもなく、繁忙期でも冬場に体育で走らされるくらいの感じです。平和。

このとき「睡眠は設計で担保するのだな~」などと思ったことがあります。

まぁその睡眠はMinecraftによって打ち砕かれるんですけど。

よくある機能強化

ライブラリ、ユーティリティ、基盤プログラムや社内フレームワーク、呼び方は色々あれどソフトウェア製品には重要な部品というのがいくつかあるものです。

きっかけはちょっとした要望だったかもしれませんが、伝言ゲームが進むとこういった重要部品を強化できないか?という話が山のように出てきます。毎度毎度の一大イベントですね。

強化で大切なことはたくさんありますが、ソフトウェアエンジニアを生業としている者としては「ユーザが使いやすく高速で保守しやすいプログラムを手早く書くぞ 今日は帰ってMinecraftで何作ろう?」ということを一番に考えて仕事をしていました。

今思えば、「普通に設計していればここにこう手を入れるよね、もうある機能を組み合わせるだけだし」という感じの話をしたようなしていないような気がします。

何はともあれ、一度やると決めたらひと思いにズバッとやってしまえば良いのです。
レビューと検査でちゃんとチェックするんできっと大丈夫です。ネコを信じよ。

がんばって修正、レビューして、ビルドもテストも問題なし。
かくして今回の強化も無事に乗り切ったのでした。

…本当に?

何かに気づいた日

少し時が流れ、ある日製品のフルビルドをすることと相成りました。

めでたくランタイムがメジャーバージョンアップされ、高速化の恩恵を受けたり新機能を使えるチャンスを得たわけです。
これはなんとしてもやりたい。だって面白そうだから。

あ、ちなみに開発ツールはVisual Studioですが、ソリューション開いてビルドボタン押して終わりというわけではありません。
なんせ「ソリューション」が数千あるのです。1個1個開いてビルドできるわけもなく、この作業は丸ごと自動化しないとどうにもならないことはD言語くんの目にも明らかでした。

まずはビルド順を決めるためにちょっとしたスクリプトを書き、依存関係グラフを作成、トポロジカルソートとやらを使って並び替えてやるとビルド順が求まります。何やらトロピカルな感じがしますね。

そしてスクリプトを実行してドーンとスタックオーバーフローを起こします
え???

そう、何を隠そうグラフがぐるぐる循環依存しているところがあるのです。先人達の設計とはなんだったのか?

いやいやせっかく面白そうなおもちゃが目の前にあるのに大切なプログラムがビルドできないとか泣きそうですマジで:;(∩´﹏`∩);:

困ったときの筋肉

一度落ち着いて考えてみると、そもそもなんちゃらソートなんてしなくても依存関係列挙するだけでグラフは生成できるのでした。Graphvizは神。

いざグラフを見てみると、一番低レイヤーとされるライブラリの1つが論理的に1つ上の層の部品を参照していました。1か所おかしいところがあれば順繰り参照される運命にある部品たちですので、途中に6個ほど部品を挟みながらぐるーっと一周して循環依存のできあがりです。なんじゃこりゃ。

とまぁ調べると完全に最近の出来事で我々の自業自得なので、夜更かしMinecraftはこれが直るまで自粛しようと決意しました。

しかし状況がわかれば対処のしようもありますし、筋肉はすべてを解決すると言われています。
私は筋肉ないので、凡人なりの解決ざっくり5ステップを編み出しました

  1. まずは下位レイヤーで、誤って参照している上位モジュールへの参照を消します。
  2. 参照を消したらエラーになる実装を1回すべて空にします。
    (関数の戻り値など消せないところに出てくることがなくてラッキーでしたが、出てくるようだと更に数ステップ必要そう)
  3. この時点でビルドできることを確認し、一旦保存しておきます。
    ※ここを起点に元に戻していくのでセーブは大切です
  4. 2で消した機能を上位の部品から切り出して下位レイヤーへ移動、上位の部品から参照するようにします。
  5. 改めて下位レイヤーで括り出した部品を参照し、空にした実装を戻して整えます。

作業が終わったら再度グラフを出力し、すべての循環依存が解消されたことを確認して完了です。
リファクタリングたのしい

これで夜もぐっすり眠れますしMinecraftも解禁、最新機能で楽しく開発できる日々を期待しましょう!
(あれから数年、まだ来てないみたいなんですが、きっと来ますよね!)

振り返りと対策

循環参照というと、「設計が変な時に大抵2つの部品間で起きるもの」というイメージでした。

しかし今回は6つも7つも絡んで一周しているため、かなり幅広い人々が長期的に関わっていないと「認識することすらできない」という結構レアで厳しい問題だったと思います。

色々がんばって無事にバージョンアップはできたわけですが、とにかくビルド用のスクリプトを直したり、人と話してどうすんだこれ、ということで2日近く揉めていた記憶があり、そこから更に修正作業でビルドが終わるまでの工数たるや凄惨たるものです。

反省として、「人間見えないものとは戦えない」「人間あるものは何でも使おうとする」という2点を自覚しました。
そして「モジュールや機能の依存関係を可視化し、常に見えるようにする必要がある」と思い至ります。
グラフを見れば一発なんですよね。作業完了の判定にも使えましたし。

そんな依存解析グラフを出力するためのツールが今回作った ddeps です。(やっと戻ってきた)

依存関係を眺める

というわけで、あらためてツールの機能紹介です。

といっても出力されるグラフを眺めれば大体お気持ちを察してもらえると信じています。

出力例

基本形

まずは自作の rx というライブラリに組み込んでみた結果です。

DでReactive Extensionsをやるためのライブラリで、25個のモジュールがあります。dubパッケージとしては中くらいの規模だと思います。

去年からまとまった強化をしている関係で、グラフにしたときに多少映えるバージョン間で差分を取ってみました。

一番上が import rx; に対応したトップモジュールで、そこから順に依存先に向かって矢印が伸びます。

なんとなくレイヤーが分かれている感じが伝わるでしょうか?
線が多いのでごちゃごちゃして見えますが、線の崩れはほぼなく割と綺麗なほうです。

細かく見ると標準ライブラリの利用状況を知ることもでき、差分にあたる部分は色が付いています。

レビューのときにこれを見れば、

  • 何を追加したのか
  • 何を消したのか

が遠目からでもざっくりわかるようになります。

標準ライブラリを除外

Dの標準ライブラリは stdcore という名前で始まるので、その範囲を除外した例です。ツールとしてそのあたりのフィルタ機能もつけています。

先の例と比べるとかなりスッキリしてノイズも少ないので、ライブラリのバージョンアップ紹介なんかにはこちらのほうが見易くて良い感じだと思います。

試しに変な参照を追加してみる

まずはサンプル用に比較的綺麗な状態を挙げておきます。

これに最下層のレイヤーから全然関係ない上位レイヤーを参照するようにしてみます。

なんとなく下方向に伸びましたが、そもそも右端やら左端やらの線が崩れ気味ですね。
色々無茶な設計をすると起こりがちな現象で、こういうときは大体何かがおかしい合図です。

伸びた下のあたりでそれとなく循環参照していることも見て取れます。

しかし、循環参照してもあんまりレイアウトが変わらない場合もあります。

この場合は色がついてるのでわかりますが、最下層右側から1つ上に向かう矢印が増えています。
元から大量の矢印が向いているモジュールはGraphvizのレイアウト的にとても動きづらいので、ちょっと残念ですが普通に起こりえる現象です。

こういう時にポイントとなるのは「矢印の向き」で、「上向き」というのが注意のサインです。
「モジュールを表す丸の下側に突き刺さるような矢じり」を探すと結構すぐに見つかると思います。

適当なライブラリに適用してみる

dubパッケージの中でも比較的有名でちょっと規模も大きい「mir-algorithm」というライブラリに適用してみます。

ちなみにこれはテンソルとか数値計算アルゴリズムをいくつか提供するパッケージです。

しばらく前に参照カウント系のモジュールが増えたり非推奨モジュールの整理があったりしたので、そのあたりの概要をつかむような目的でも使えます。

ちらほらと下から上に向かう矢印もありますが、追加されたあたりは良い感じに塊になっているのでわかりやすいですね。
先ほどのような左右に無理な曲がりかたをする線も見当たらず、規模の割に綺麗な設計なのだろうと推測されます。

なんとなくレイヤーらしきものが感じ取れる気もしますが、線が多くて…密度が…(課題です)

コンセプト

グラフを見てもらえば大体終わりなのですが、このツールを作成するにあたって目標に据えていたコンセプトが3つあるのでまとめておきます。

  • 「結果を見ることで綺麗な依存関係を作れるようにする」
  • 「広く知られたメタ知識を活用して初見でもわかるようにする」
  • 「ライブラリ作成を面白くする」

まずは何よりグラフにすることで、「見た目の綺麗さ」から「設計の善し悪し」が直観的にわかるようにしたい、という気持ちがありました。
「綺麗な設計ならグラフにしても綺麗である」というのが当初の仮説ですが、確かにうまく設計すると依存関係が全体的に流れ落ちるような構造になったり、モジュールのまとまりや階層構造が見えてきたりします。

また、グラフ要素の差分を表現するにあたり、GitHubのコード差分を表わす色合いをほぼそのまま持ってきました。
増えたら緑、減ったら赤、という「みんなそれとなく持っている『メタ知識』の活用」ということで、思ったよりわかりやすくなった気がしています。私が知らないだけで似たツールいっぱいありそうですし。

あとはREADMEに組み込み手順をまとめたりして、dubパッケージとして登録して結構簡単に使えるようにしたつもりです。(もう少しコマンド減らせるように機能強化したほうが良さそうですが)

手元のライブラリに適用するとパッとグラフが出てくる、という開発体験はなかなか面白いのではないかと思っていて、今後も同じようなコンセプトでプログラムを解析できるツールを作っていきたいと思います。

仕組み

D言語のコンパイラが持つ依存関係の出力機能を使ってファイルを用意、ツールでGraphviz向けのdotファイルを生成、Graphvizを使ってグラフにする、というのが基本的な流れです。

D言語のコンパイラは主にDMD, LDC, GDCの3つがありますが、この依存関係を出力する機能はDMD/LDCで共通の -deps というフラグを指定すれば出力することができます。個人的にこのファイルを「depsファイル」と呼んでいます。
※GDCにも -fmake-deps というフラグがありますが動作は未確認です

最初は静的解析でやろうとしたのですが、このファイルの存在に気づいたらアルゴリズムをちょっと整えるだけで済み、1日ちょっとで完成させられました。

また、差分表示にはWebでよくあるスナップショットテストを参考にしているのですが、前のグラフを保存しておく必要があるのでdepsファイルをdeps-lockファイルとして横にコピーして置いておきます。
あとの差分の計算は単体テストと気合です。(プログラム全体にテストもコミコミで600行ちょっと)

というわけで、ツール構成とデータの流れを一通り図にしてみるとこんな感じです。

使い方

やることは、依存関係の出力、lockファイルの更新、差分のグラフ生成、という3つのタスクです。

事前にGraphvizをインストールしてパスを通した状態にし、後のコマンドを簡単にするため dub.json に以下のような configuration を書き足しておきます。

1か所 diff のところで --focus=rx と書いてあるところは設定するライブラリの最上位にあたるモジュールの名前を設定してください。

"configurations": [
    {
        "name": "default"
    },
    {
        "name": "diff",
        "postGenerateCommands": [
            "dub build -c makedeps",
            "dub fetch ddeps",
            "dub run ddeps -- --focus=rx -o deps.dot",
            "dot -Tsvg -odeps.svg deps.dot"
        ]
    },
    {
        "name": "diff-update",
        "postGenerateCommands": [
            "dub fetch ddeps",
            "dub run ddeps -- --update"
        ]
    },
    {
        "name": "makedeps",
        "dflags": ["-deps=deps.txt"]
    }
]

使うときは、

  1. 初回のみ dub build -c makedeps として依存関係ファイルを生成、dub build -c diff-update でlockファイルを生成する
  2. dub build -c diff でグラフを生成、眺める
  3. 何かモジュール構成や依存関係を変更が必要なら作業して2に戻る
  4. これでOKとなったら dub build -c diff-update としてlockファイルを更新する

という感じです。

まとめ

  • 大規模になるほど循環依存の問題が見つかったときは大変
  • 人間見えないものとは戦えない
    • 特に循環依存に関しては「発生の瞬間に気づけない」こともある
  • 人間あるものは何でも使おうとする
    • 便利なものは1か所に置きたくなるけど心を鬼にして小さく分けよう
  • D言語にはコンパイラ組み込みの便利機能が色々ある(-depsフラグ)
  • Graphvizは神

おわりに

まだまだ改善点は多くありますが、個人的に今年1番満足度の高いツールになりました。(未だに色々遊べるので)

また今回は使い慣れた言語なこともあり、順序付き集合の差分計算まで標準ライブラリで済むなどの恩恵をフルに受けられました。やはり何事も慣れていると強いですね!

というわけで、こういった依存関係の解析ツールで目の前のプロジェクトが少しでも楽しく保守できて、より良い設計を目指す手助けになれば幸いです!

ツール作りもプログラミングも面白いし、D言語を使うのももっと楽しくなってほしい!!!