オータムマガジンにトーノZERO氏参戦で、オータムマガジンの最上位の表紙を見ると、まるでアニメ感想サイトのように見えかねません。というわけで、トーノZERO氏に負けないように、ヘロヘロな身体を鞭打って、硬派の (でも本当はミーハーな) ソフトウェア関連コンテンツを行きます。
学生時代に考えたこと §
ここでは、学生時代にプログラムの複雑さについて考えていたことを、書いてみたいと思います。
この話は、次に書かれるであろう「C言語を用いたモジュールプログラミング入門」の布石となるものでもあります。
ちなみに、これは私が「学生時代に考えたこと」であって、この考えがどれぐらい適切なものであるかは分かりません。考えたことと言っても、いろいろな本から受けた知識や印象にかなり依存している部分があると思います。つまり、完全にオリジナルというわけではありません。
複雑なプログラムとは何か §
複雑なプログラムとは、ソースコードが複雑に絡み合っていて、一読しても何をしているのか把握できないようなもの、とします。分かりにくいプログラムと言っても良いと思います。分かりにくさは、仮に「複雑度」という指標で表せるものとして話を進めます。
ちなみに、複雑なプログラムには、必然的に複雑になるプログラムと、不必要な複雑さを含んだプログラムがあります。前者は、要求を実現すると、複雑にならざるをえないもの。後者は、もっとシンプルに作成することができるのに、複雑になってしまったものです。
複雑なプログラムはなぜ良くないのか §
複雑なプログラムはさっと見て理解できないので、読みにくいと言えます。しかし、読みにくいことだけが欠点ではありません。たとえば、ソースコードを書き換える場合に、どのように書き換えれば適切かすぐ分からず、どう書き換えれば安全であるかの保証もできにくいのです。プログラムの仕様は開発中にも、完成後にも変更される場合があるのが普通のことで、既に書かれたソースコードの書き換えは必ず発生します。そのような状況で、不必要な複雑さに絡め取られて身動きができなくなることは、無駄の極みです。
用語の定義 §
ここではいくつかの用語を使います。特定のプログラム言語の用語を使うのも面白くないので、ここでは以下の用語を使うことにします。
- 「構成要素」 C言語でいう関数や変数など
- 「モジュール」 プログラムを構成する単位。C言語ならソースファイル1つ。JavaやC#ならクラスなどに対応すると考えてもよい
何が複雑さを生むのか §
はたして、プログラムの何が複雑さを生むのか。
学生時代に考えたことは、プログラム内のあらゆる構成要素に対して、無差別にアクセス可能である点が複雑さを生むと言うことでした。
たとえば、作業用の一時的な変数や、関数の作業の下請け用に作った限定的な関数などに自由にアクセスできると、無関係なコードからそれを使ってしまうかも知れません。作業用の一時的な変数が参照されてしまうと、その作業のアルゴリズムを変更できなくなるかも知れません。そのようなソースは、不必要な複雑さを含んでいると言えます。
しかし、アクセスして良いものと、そうでないものを、明示的に識別する方法はあるのでしょうか。昔のBASIC言語にはありませんでした。しかし、より新しい世代のプログラム言語では、そのような制御を積極的に行う手段があります。それは、今となっては常識なので、くどくど書くことではありませんね。
複雑度を定義してみる §
さて、問題は、構成要素間のアクセス制御を行うことで、どの程度の複雑さを取り除けるかです。それを考えるためには、1つの指標となる数値を導入してみます。
ここでは、これを複雑度と呼びます。以下のようなものであるとします。
複雑度とは、プログラム内の個々の構成要素がアクセス可能な構成要素の個数の総和とする
この複雑度の定義がどれぐらい適切であるかは分かりません。たぶん、これがあるべき適切な定義ではないでしょう。ですが、何も指標がないのも分かりにくいし、このコンテンツは「私が学生時代に考えたこと」を書くものなので、これを使って行きましょう。
たとえば、構成要素を100個含むプログラムがあるとします。アクセス制御が全く行われていない場合、個々の構成要素は自分を含む100個の構成要素へのアクセスが可能です。ですから、このプログラムの複雑度は、100×100=10000となります。
2003年11月1日追記 §
この計算に疑問を投げかける意見があったので補足説明します。100個の構成要素があるとき、ある1つの構成要素が関係を持てる相手となる構成要素は、99個であるはずだ、と考えることができます。100個のうちの1つがアクセスを行う側に立てば、それを受ける対象となるのは、100-1=99個となるわけです。しかし、再帰呼び出しのテクニックを使うと、自分自身を呼び出すことができるので、自分が自分に対してアクセスする関係があり得ます。ですから、ある1つの構成要素が関係を持てる相手は、自分自身を含め100個となります。このような考え方に則って、上記の計算は行われています。
モジュールによって複雑度を軽減する §
では、複雑度を下げるにはどうすれば良いでしょうか。より具体的には、複雑度の数値を下げるにはどうすれば良いでしょうか、と言うことになります。
ここで、アクセス制御について考えます。
全ての構成要素が、他の構成要素にアクセス可能か否かの情報を付け加えれば、複雑度を軽減できるように思われるかも知れませんが、それは違います。それを行っても、結局のところ、アクセス指定を行う手間が増えるだけで、複雑度が下がるわけではないのです。
ここで必要とされるのは、構成要素をグループ化し、グループ内でのみアクセス可能という範囲を創り出すことです。この範囲を、ここではモジュールと呼びます。たとえば、ファイル処理のモジュールと、通信処理のモジュールがあったとき、ファイル処理にしか関係しない構成要素は、通信処理のモジュールのような他のモジュールからアクセスすることができないようにします。しかし、ファイル処理にしか関係する構成要素からは自由にアクセス可能にしておかないと不便になってしまいます。このような範囲を創り出すことが、複雑度を軽減することに役立ちます。
たとえば、構成要素を100個含むプログラムを、構成要素を10個含む10個のモジュールに分割できたとします。個々のモジュールの中で、半数の構成要素は、外部のモジュールからアクセスを許さないようにするとします。その条件で複雑度を計算してみます。
まず、外部のモジュールからアクセスできない構成要素は、5個*10モジュール=50個あります。これらの構成要素は、同じモジュールに属する10個の構成要素にしかアクセスできないので、これらの持つ複雑度は、50個*10個=500ということになります。
さて、外部のモジュールからアクセスできる構成要素は、5個*10モジュール=50個あります。これらの構成要素は、同じモジュールに属する10個の構成要素と、他のモジュール9個が持つ公開された5個の構成要素にアクセスできます。ですから、複雑度は、50個*(10個+9個*5個)=2260となります。
両者を足すと、このプログラムの複雑度は500+2260=2760となります。
モジュールを導入していない場合は、10000でしたから、複雑度はわずか28%程度にまで減らすことができたことになります。
しかし、これはまだ最終解ではありません。
モジュールの明示的な参照設定 §
ここまでの例は、モジュールが構成要素を隠蔽できる、という機能だけを前提としています。しかし、これだけではまだ不十分です。あるモジュールが参照するモジュールを明示的に指定し、その他のモジュールへのアクセスは無いと指定することで、更に改善を図ることができます。
たとえば、構成要素を100個含むプログラムを、構成要素を10個含む10個のモジュールに分割できたとします。個々のモジュールの中で、半数の構成要素は、外部のモジュールからアクセスを許さないようにするとします。ここまでは、上の例と同じですが、更に条件を付け加えます。あるモジュールは、自分を含む5個のモジュールのみを参照する(つまり他のモジュールは4個参照する)という明示的な指定を付け加えているとします。その条件で複雑度を計算してみます。
外部のモジュールからアクセスできない構成要素については上と同じ計算になり、500となります。
外部のモジュールからアクセスできる構成要素は、5個*10モジュール=50個あります。これらの構成要素は、同じモジュールに属する10個の構成要素と、他のモジュール4個が持つ公開された5個の構成要素にアクセスできます。ですから、複雑度は、50個*(10個+4個*5個)=1500となります。
つまり、隠蔽だけを行った場合、複雑度は28%まで減少したのに対して、明示的な参照を設定すると、15%にまで減らせるというわけです。
このような、複雑度の減少を行うことができるプログラミング手法を、モジュール化プログラミングという、と私は理解しています。まあ、プログラム言語ミーハーの理解ですから、かなりインチキですが。試験の答案に書くときは、ちゃんと教科書で調べて下さいね。
モジュール化プログラミングは実践できるか? §
では、このようなモジュール化プログラミングは実践できるでしょうか。
C言語を使うと、ある程度実践できます。(時間があれば次に書く……かもしれないコンテンツで具体的なやり方を解説します)
JavaやC#のような系列の新しい世代のプログラム言語では、たぶん、実践できません。
それはなぜか。
モジュールはクラスに相当すると考えた場合、明示的な参照設定ができないからです。これらのプログラム言語は、(大ざっぱに言えば)同じプログラム内のpublicな構成要素に全てアクセスできます。それを制限することはできません。C#の場合、アセンブリを単位にしてアクセスを制限することができますが、制御単位が大きすぎ、かつ、別のアセンブリに単体テストを書き込む場合、アセンブリ内のみにアクセスを制限すると、テストから呼び出せなくなってしまうので、あまり実用的ではありません。
というわけで、実は万事解決オッケーだぜ!という結論にはならないわけです。
専門家がどのように考えているか分かりませんが、実はこのへんが個人的な不満点であったりします。