みなさん、メンテナンスしやすいプログラムにするための設計に苦労してませんか? 次々と現れるフレームワークやデザインパターンに振り回されてませんか!?
プログラムの設計については、パターン周りを中心に長い間多くの人を悩ませているように見えます。例えば MVC などは 1980 年代からあるものなのに、未だに定期的に議論に上がったり改善されたパターンなどが提案されたり、それを元にしたフレームワークが現れたりします。
僕もそういった設計に悩んだ(でる)一人なのですが、最近は新しいパターンも大事とは思いつつも単純に依存関係をきちんとコントロールする事が大事なんじゃないかと思うようになってきました。
そこで、この記事では依存関係をテーマに見通しが良く変更に強いプログラムにするために重要だと思う事を書いていきます。
この記事は大きく前半と後半に分かれています。前半は依存関係そのものの話や疎結合についての話です。後半は依存関係をテーマとして重要だと思う指針を書いています。
で、君、誰?
ネイティブアプリや Web アプリ開発をやっています。ここ数年は Java や Swift を使ったネイティブアプリや、Java のAPIサーバーを書くことが多かったです。
一時期はオブジェクト指向プログラミングや設計の本とかも読んでいてこういった本の影響は大いに受けていますが、正確な内容は大体忘れています。なのでこういった本で書かれていることを(独自解釈を加えて)解説したり使用したりする事は避けているつもりですが、誤解を招きそうな表現があれば教えてもらえればと思います。
プログラムが持つ依存関係
修正しやすく要件などの変化に強いプログラムの構成のためには、プログラムの各構成要素が何に依存しているのかをうまく把握・管理する必要があると思っています。
プログラムの構成要素の依存性
プログラムの要素が依存する物といえば、多くの場合は以下のような物を指す事が多いと思います。
- 参照している他の構成要素(引数や他のクラスなど)
- 使用しているライブラリやフレームワーク
しかし実際にはもっと多くの物に潜在的に依存しています。例えば以下のようなものがあります。
- 使用している言語
- プログラムにおけるルール。MVC などのパターンやコーディング規約など
- そのコンポーネント自身が持っているプログラム要素
- クラスの場合、自身のインスタンス変数やメソッド
- 特定の状況にのみ起こるバグとそれを回避するためのハック
- プログラムの要件
- プログラムのユーザー
中には使用言語などのようにプログラミング中に考えても仕方ない依存もありますが、特殊なロジックや特殊な事情等はプログラム要素に不安定な依存を暗黙のうちに持ち込んでいると言えるでしょう。
依存関係の伝搬
プログラムの構成要素が依存している対象に何か変化が起きた場合、依存している側が壊れるリスクが発生します。これは、間接的に依存している要素にも言えます。つまり、変更が依存関係をたどって伝搬します。
依存関係が複雑だと一つの変更のために広範囲の修正が必要になっていくので不要な依存関係を減らしていくのが大事ですが、アプリケーションが大きくなるにつれ全体の依存性はどうしても大きくなっていきます。
そこで伝搬を抑えるための工夫が必要になってきます。
ルールを導入して変更の伝搬を抑えよう
プログラム要素の変更が依存関係をたどって伝搬させないために一般的によく使われる方法があります。
その方法というのは、プログラム要素に対して「満たすべきルール」を決め、「そのルールを満たす限りは要素の変更を許容する」というものです。
オブジェクト指向の分野でいう「契約1」と呼ばれるものが同じアイデアだと思います。ただし、ルールは明記されているとは限らず、習慣などで暗黙的にできているものもあります(get〜メソッドではオブジェクトの変更は行われない、など)。
そしてルールを導入することで見た目の依存性が小さくなった事を疎結合と呼ばれる事が多いです2。
「疎結合なシステム」は依存関係が少ないわけではない
ルールを導入する事で潜在的な物も含めた依存性は大きくなります。ルール自体に依存する事になりますし、ルールを導入するために別の新しい仕組みなどが導入される事も現実には割と多いと思います。例えば、オブジェクト指向言語の場合はルールを導入するためにインタフェースやプロトコルを導入したり、そのために新しくパッケージを用意する事がよくあると思います。
また導入したルールが将来の変化に適応できなければルール自体を変更する事になり、変更の伝搬は防げません。例えば Java などでインタフェースを導入したのにインタフェースの変更が頻繁に起こると、ひたすら実装クラスとインタフェース行き来し続けるようになってしまいます。
一方でルールの変化を起こさずにプログラム要素の変更ができれば、プログラム全体の変更はごく小さな物で済むはずです。ルールの導入は一種の先行投資と言えると思います。またプログラム要素間のルールを一切明確化しない事も現実的ではないので、プログラムが大きくなるにつれルールの導入は増えてくるでしょう。
依存関係についての認識はこんな感じです。次は依存関係をテーマとして変化に強いプログラム構成についての考察を書いていきます。
変化に強いプログラム要素が持つ依存関係
1. プログラム要素が使われている
依存関係以前の問題でもありますが、使用されていないプログラム要素は早めに、できればすぐに削除しましょう。
プログラム要素が使用されていなくても、プログラム全体から見ると余計な依存性が発生してしまいます。依存している要素に変更があった場合、意味がないにもかかわらず修正が必要になってしまいます。
2. 依存関係が単方向である、循環しない
依存関係が相互に存在すると変更に弱くなりがちです。依存関係が相互にあると、どちらか一方の変更でもう一方が壊れるリスクが発生します。また3要素以上でも循環するような依存関係が発生していると、どれかに変更があるとその他の全てが壊れるリスクが発生します。
また相互に依存している要素を変更すると、交互に何度も反復しながら修正する必要が出てくるので、修正も手間になります。
中間に仲介する依存関係を持つことで単方向にできます。関数オブジェクトやオブジェクト指向言語のポリモーフィズムを使いながらルールを導入することも多いです。
なお相互の依存関係は無理に削除する必要はなく、要素を近い場所に配置するだけでも修正の手間は削減できます。
3. 依存性の大きい要素ほど使われる場所が局所的
依存性が大きい要素ほど変更が起きるリスクも大きいです。そういった要素がいろんな場所で使われると、変更が伝搬する事も増えてしまいます。
4. 依存性の小さい要素ほど多くの場所から使われる
プログラムの各要素の依存性が小さく抑えられていても、十分変更に強いとは限りません。
例えば上の図の左のように、各要素が一箇所でしか使われていないものばかりだったケースが相当します。こういった場合、依存される側は依存する側の事情に依存している事が考えられます。言い換えると、逆側の依存関係が潜在的に発生している事になります。こう言ったケースの場合、同じ要素に統合した方が良い可能性があります。
逆に、依存性の小さい要素が多くのもので使用されるということは、汎用的に使える上に変更リスクの小さい効果的な要素であると言えます。
情報エキスパートパターンとモジュールの大きさ
少し横にそれますが、「実践 UML」という本に GRASP というパターン集が紹介されています。その GRASP の中には「情報エキスパートパターン」というものがあります。
ざっくり言うと「新しいメソッド(正確には責務)を追加する時、どのクラスに割り当てるべきか? その遂行に必要な情報を持つクラス(情報エキスパート)に割り当てるべし」というものです。これを読んだ時は感動しました。
当時よく見ていたプログラムは「クラスやメソッドが膨れ上がろうが構わず全部一緒くたにする(機能追加しようとするたびにどんどんカオスになる)」ものと「とにかくインタフェースやクラスに細切れに分離する(機能追加に必要な定型文が多すぎてうんざりする)」ものが多く、どちらも修正が苦痛なものでした。すでに必要な情報を持っている「情報エキスパート」がある場合はそこに処理を追加すればよく、そうでなければクラスを新しく追加するなりクラスを分割するなりする。そう言ったバランスをうまく表した良いパターンだと思っています。
今回の依存性の話に結びつけると、新しくメソッド等をどこかに追加する際には以下のようにすると良いと思います。
- その新しいメソッド等自身が持つ依存関係を考える。
- その依存関係をすでに保持しているクラスやモジュールがないかを探す。
- ある場合 → それに追加する。
- ない場合 → 新しくクラス等を作るか、クラス分割などの再設計を考える。
個々の依存関係の話はこれで終わりです。次はパッケージのような複合的なプログラム要素に注目していきます。
変化に強いプログラムが持つ依存関係の構造
個々の依存関係が良好な状態だとしても、プログラム全体が持つ依存関係はどうしても複雑になってしまいます。そこで、複雑な構造でも全体をコントロールしきれるような構造にしていくことが重要になっていきます。
以下に見通しが良く変化に強いプログラムが持つ依存関係の構造を書いていきます。
1. 要素がグループ化、階層化されている
例えば、
- クラスやモジュールが大きくなったら → 分割する
- ディレクトリやパッケージにクラスやモジュールがたくさん並んだら → グループ化する
- ディレクトリやパッケージがたくさん並んだら → 更に上の階層を作る
きちんとプログラムの要素が階層化されていることで、外観と詳細の両方の視点で依存性を見る事ができるようになります。
ただし単にグループ分割して階層化すれば良いという事にはなりません。見通しの良い階層とそうでない階層があります。
2. グループ間を見た場合にも単方向の依存性が維持されるようになっている
グループ間を見た場合にも依存が単方向であることが重要です。以下のようなメリットがあります。
- 各要素が何に依存しているのかが、所属するグループを見れば大体理解できる
- 双方向の依存があったとしても同じグループにあれば切り替えが早い
階層のトップレベルのグループが単方向の依存性を維持されている事を「レイヤー化されたアーキテクチャ3」と言われたりします。下位レイヤーから上位レイヤーへの依存関係が発生しないことが特徴となっています。
3. 上位レイヤーが要件を反映している
単方向に依存関係が成り立っているレイヤー化が進めば自然と上位レイヤーがハイコンテキストになり、汎用的なものは下位レイヤーに集まる傾向になると思います。
そしてある時点より下位のレイヤーは技術的な関心事が中心になり、上位のレイヤーは要件を表すようになってくるでしょう。
4. グループのリファクタリングを恐れない
これは設計というより開発体制の話になってしまいますが…。
グループ化を行う際、最初から正解にたどり着くのは難しいと思います。またプログラムが大きくなるにつれ、階層構造は実情に沿わなくなっていくと思います。そのため、きちんと実情に合わせてリファクタリングできるような体制になっている事が必須になってきます。
修正範囲が多くなるので最初は危険に感じる事が多いと思いますが、静的型付き言語であればビルド時に検出されるのでリフレクション等に気をつけさえすれば安全に変更できます。動的型付き言語の場合は勝手に検出できないケースもあるかもしれませんが、ほとんどの問題はユニットテスト等で簡単に見つかると思います。ディレクトリ変更しただけで修正に膨大な手作業と手動の確認が必要になるようなプラットフォームに依存している方は心を強く持ってください。
まとめ
プログラムが持つ依存関係について普段思っている事をまとめました。修正を恐れずにかつ安全に開発運用をするためには、開発者が依存関係を明らかにしコントロールする事が必須だと思っています。特に、階層構造のトップの方のパッケージやディレクトリは定型文に従うのではなく、開発者自身がきちんと管理するべきものだと思います!
-
契約については「オブジェクト指向入門」参考。 ↩
-
結合度を下げるために必ずしもルールを導入する必要はないとは思いますが、「疎結合」と呼ぶ場合は結合度を本来よりも下げるために情報を隠蔽する仕組みを導入するという文脈で使われる事が多いと思っています。 ↩
-
レイヤー化 … レイヤー化アーキテクチャは「エリック・エヴァンスのドメイン駆動設計」に詳細が書かれています。また「実践 UML」にも詳しく紹介されています。 ↩