空間軸でつくる!

「ソースファイルを分割するときは、スコープを意識するとメンテナンスがしやすくなる」と教えてもらいましたけど、それ以外にも意識すると良いポイントってありますか?
もちろんあるわよ。……それを説明するには、まずは「モジュール」の基本的な考え方について整理しておきましょうか。
「モジュール」ですか!?

ソフトウェアを設計するとき、関連性の高い部分をひとまとめにしたものをモジュールと呼びます。どのようにモジュールに分割するかは、設計における重要な論点です。

一般的に、モジュール内の各機能の関連性が高く(=モジュール強度が強く)、ほかのモジュールへの依存度が低い(=モジュール結合度が低い)ほど良い設計とされています。

各機能の関連性と、モジュール同士の依存度かぁ。……それって、「情報隠蔽」を保つようにスコープを意識しておいたら、自然と良い設計になるってことですよね?
正解!……でもね、モジュールが実装上のどの概念に相当するかは、採用するプログラミング言語の種類によって変わってくるのよ。

オプジェクト指向をサポートする言語では、オブジェクトがモジュールに相当します。一方、C言語が主流だった時代は「どの機能をどのソースファイルに収めるか」というのがモジュール分割の論点でした。

オブジェクト指向については、別のシリーズで詳しく説明します。

いずれにしても、ソフトウェア開発は要求される処理を洗い出すことから始める場合が多いのではないでしょうか。実現すべき処理がリストアップできたら、それらの詳細を明らかにして、モジュールに割り当てていく流れです。

このとき、「まずああして、次にこうして……」と処理の順番を考えて、「時間軸」に沿った設計をしがちです。でも、ある程度大きなソフトウェアを設計するときは、この手法がうまくいく見込みは少ないでしょう。

え、そうなんですか!?
必要な処理の洗い出しを上流工程で完了させられるだけの実力と、かなりの幸運が重ならなければうまくいかないんじゃないかしら。

なぜなら……

  • 要求は増えたり変わったりする
  • 事前に予測できるのは、それらの変更の一部に過ぎない
  • そうして生じた設計の「ひずみ」を、根本的に解決するための時間的な余裕はない
  • その結果、徐々にソフトウェア全体が複雑化していく

こうして、はじめのうちはモジュールが適切に分割されていたとしても、分割すべき場所がいつの間にか変わってしまうことが少なくないのです。でも、分割の境界線を変えるには大きなコストがかかるので、やり直しがきくことは多くはありません。

そうこうしているうちに、どのモジュールが何の処理をするためのものだったのかが分からなくなり、ソフトウェア全体が一つの巨大なモジュールのようになっていきます。複雑すぎて手に負えない迷路のような状態です。

うわぁ、なんだか身に覚えがあるような。
こうした問題を克服するには、「時間軸」で考えるのをやめて、「空間軸」で設計するのがおすすめよ。
……空間軸、ですか?
処理の順番(時間の流れ)ではなく、求められる役割の違い(空間的なつながり)に注目してモジュールの境界線を決定するの。

「オブジェクト指向」について調べたことがある人は、「アクターモデル」という用語を知っているのではないでしょうか。これは、ソフトウェア全体を「アクター」で構成する設計手法です。アクターは、それぞれが自分の役割をもっていて、互いにメッセージを送り合って協調的に動作します。

アクターは、「役者」という意味ですね。
そう。そこで、ソフトウェアを演劇だと思って考えてみましょう。

もし、演劇を「時間軸」で設計するとしたら、「第1幕」や「第2幕」などのモジュールに分割することになります。でも、完成するまでにストーリーは変わるかもしれません。「第1幕のあのセリフは第2幕にもっていこう」などとやっているうちに、モジュールの境界線は曖昧になっていきます。

これに対し、演劇を「空間軸」で設計するのは、「役者(アクター)」でモジュールを分割するということです。舞台というソフトウェアを構成する主要なオブジェクトは、役者たちだからです。それぞれがメッセージを送り合い、自身のキャラクターを演じることによって舞台が進行していきます。

このときポイントとなるのは、役者は舞台上に間違いなく存在する「実体」で、それぞれが「役割」を与えられているという点よ。
なるほど〜。
あと、各自が自律的に振る舞うことによって、ほかの役者とも立派に共演を果たしているわね。
ふむふむ。

ソフトウェアを空間軸で設計するのは、演劇の登場人物をデザインするのに似ています。全体をアクターに分割し、それぞれに役割を与えるのです。これは、ソフトウェアを実在の「もの(=オブジェクト)」の集まりとしてとらえるということです。

オブジェクトごとの役割が明確であれば、要求を満たすために必要な処理を割り振るのも難しくはありません。あとから要求が増えたり変わったりしても、どのオブジェクトに対応させるべきかは明らかでしょう。

演劇で少しくらいストーリーが変更になっても、役者のキャラクターがガラリと変わるわけではないのと同じですね。
そういうこと。その結果、モジュールの境界線が曖昧になっていく問題を避けやすくなるの。

また、このような役割に応じた分割手法には、関連する実装が自然と近くに集まってくるというメリットもあります。必要以上の労力をかけずに、モジュール強度が強く、モジュール結合度が低い状態を保てるのです。

コーディングをする際も、1つのオブジェクトだけに注目すれば済む場合が増えるでしょう。

ソフトウェアをオブジェクトの集まりとしてとらえる、っていう考え方は分かったんですけど……
疑問があるのね?
はい。それって、C言語だとできないってことになりませんか?
なるほど。C言語はオブジェクト指向言語ではないものね。
そうですよ!だからC++にしないと無理なのかなって……
そこは大丈夫。C言語でも同じような考え方はできるのよ。

開発に使う言語をC言語からC++に移行すれば、ソフトウェアをオブジェクトの集まりとして設計しやすくなるのはたしかでしょう。とはいえ現実には、簡単に移行できない場合も少なくないはずです。

それでも、C言語で同様の設計をすることは可能です。「オブジェクトで考える」というのは、「オブジェクト指向言語を使う」こととイコールではないからです。

C++では、オブジェクトの種類をクラス(class)で定義します。クラスは、そのオブジェクトの内部状態を示す変数と、外から見たときの動作を決めるものです。これにより、それぞれのオブジェクトは自律的に振る舞いながら、自分の役割を果たします。

C言語でこれと近いことができるのは構造体(struct)です。オブジェクトの内部状態を示す変数は、構造体に直接含められます。オブジェクトの動作は、構造体を扱う関数として定義すればよいでしょう。

このとき、構造体ごとに別々のソースファイルを作るようにします。そうすれば、構造体を扱う関数も対応するソースファイルに自然と集めやすくなり、モジュールの境界がハッキリします。

C言語でもC++でも、オブジェクトに注目するのは自然な考え方だといえるわね。
そうなんですね。でも、僕にもできるかなぁ……
そうねぇ。プログラム中の要素を収まるべきところに収められたぞ!っていう感覚を大切にしていけば、だんだん慣れてくるんじゃないかしら。
そっか。モジュール分割がうまくできているときは、そういう感覚になりそうですね!
ここがポイント!
時間軸ではなく、空間軸で考えて設計しよう!モジュールの境界線が曖昧になりにくくなって、ソフトウェア全体が複雑化するのを防げます。