コンテンツにスキップ

スパゲティプログラム

出典: フリー百科事典『ウィキペディア(Wikipedia)』

スパゲティプログラム: spaghetti program)またはスパゲティコード: spaghetti code)とは、コンピュータプログラムの状態を指すための表現(俗語)であり、命令の実行順が複雑に入り組んでいたり、遠く離れた関連性の薄そうなコード間で共通の変数が使われていたりするなど、処理の流れや構造が把握しにくい見通しの悪い状態になっているプログラムのことである[1]。スパゲッティプログラム、スパゲッティコードとも表記される。

名称の由来は、皿に盛られたスパゲッティのように実行される箇所の線が絡み合っていることから。「パスタプログラム」とも呼ばれる。

スパゲティプログラムの直感的なイメージ。プログラミングがワイヤラッピングで行われていた時代のスパゲティコード(1977年)

概要

[編集]

命令型プログラミングでは、プログラムはコンピュータに対する手順書であり、プログラムに書かれた通りの順でコンピュータに対する指示(命令)が次々に出され、それをコンピュータが解釈することで動作していく。中間表現仮想機械を利用する形態もあるが、最終的には機械語としてコンピュータが直接解釈可能な命令に変換される。メモリ上に存在する変数などによって管理されるプログラムの状態(ステート)を動的に変化させながら、プログラムに書かれた通りにを次々に実行していく。基本的には上から下に辿る順でプログラム行が1文ずつ実行されていくが、行番号や何らかのラベルを指定して遠く離れた場所にジャンプする命令や、サブルーチン呼び出しと復帰など、遠く離れた場所にジャンプして別のコードを実行した後、また元の位置に戻る、といった命令もある。スパゲティプログラムというのは、規律のない不用意なジャンプの多用によって命令実行の順番が複雑に入り組んでいたり、プログラムの状態を管理するための変数が遠く離れた場所(プログラム全体のあちこち)で読み書きされていたりと、まさにスパゲティがこんがらがったような状態になったプログラムのことである。

スパゲティプログラムは、プログラムのテストを実施したり、内部動作解析やデバッグのためにプログラムをステップ実行によりトレース[注釈 1]したりすることが困難になる。結果として、プログラムのスムーズな開発や完成を妨げる。またソフトウェアを改良したり拡張したりすることも困難にする。

スパゲティプログラムは、特に1980年代頃[独自研究?]、まだ構造化プログラミングという知識・手法がプログラマたちに十分に普及しておらず、また仕様上構造化プログラミングが不可能なBASICのようなプログラミング言語が使われてしまっていた状況でしばしば発生した。

たとえ構造化プログラミングをサポートする現代的な環境であっても、変数名などが不適切で分かりづらいソースコード[注釈 2][注釈 3]や、複雑な条件分岐、あるいは処理内容および機能のまとまりに応じて適切に分割されていない長大なサブルーチンおよび巨大なクラスなどによって、可読性やメンテナンス性の欠如したスパゲティプログラムは容易に発生しうる。また後述するように、スレッドなどを使用した非同期プログラミングも、別の意味でのスパゲティ化を招きやすい。

ソースコードの可読性(読みやすさ)という概念は、読み手の技量や知識にも左右される相対的な指標であるため、初心者にとって難解なコードが必ずしもスパゲティプログラムとは言えない。しかし、むやみに技巧を凝らしたコードや、処理速度などの最適化のために可読性・汎用性・拡張性などを犠牲にしたコードは、一見では何をしたいのか理解できず、経験を積んだプログラマであっても内容の理解に時間を要したり誤解してしまったりすることがある。OSAPI・ライブラリの不具合を回避するためにアプリケーションソフトウェア側で仕方なく必要とされるようなコードは、一見では無意味に思えるものもある。また、たとえ自分自身が書いたコードであっても、後から読み返すときには詳細を忘れてしまっていることも多い。そのようなとき、仕様書やソースコード中のコメントに十分な説明がなかったり、あるいはコードの修正によって仕様書やコメントが実装と乖離していたりすると、たちまち解読不能なスパゲティコードと化してしまう。

サブルーチンやクラスを最初に実装したときは、整然とした読みやすいコードになっていたとしても、機能追加や仕様変更に対応するためにコードを修正し、状態を管理・保持するための変数や条件分岐などが増えていくにつれて、徐々に読みやすさが失なわれ、設計も陳腐化していき、気付いたときにはスパゲティコードになっていることもある[2]

現代では、プログラミング(コーディング)というものは、組織で業務として行う場合でも、たとえ個人的な趣味で行う場合でも、構造化プログラミングを行うべきだとされている。というのは、たとえ個人が自分ひとりのために書くプログラムでも、書いてから数ヶ月や数年もすれば、書いた時の記憶はほとんど、後から積み増しされた日々の記憶の山の中に埋もれてしまい、書いた当時に自分が何を考えて書いたか思い出せなくなり、まるで「赤の他人が書いたよう」に見える状態で読まなければならなくなるからである。日々多くの文字を読み多くの体験をしつづけている普通の人ならば、特にコードをしばしば書く人ならば、数ヶ月の間に大量のコードを読み書きすることになり、「数ヶ月前、数年前の自分」が書いたコードやそれを書いた時の理屈は、大量の記憶の中に埋もれてしまっており、すぐには思い出せず、「まるで他人が書いたコードのよう」で理解しがたいのである。だからプログラム(コード)というものは、チームで共同作業で書く場合でも、個人が個人的趣味で書く場合でも、誰が読んでも直感的に理解しやすいように、整然と、原則上から下へと素直に順番に実行されるように書くべきだとされている。またしっかりブロック化し、変数のスコープ(有効範囲)を常に意識し、たとえどんなに巨大なプログラムのどの行でも、もし変数の役割がやや不明な行があれば、せめてそのブロックの冒頭に戻れば変数の宣言も明確に書かれていて(さらにもし宣言だけでは変数の役割・機能が曖昧なら、その役割もコメント文などで明記されていて)、誰でも理解しやすいように(未来の「他人のような自分」でも理解できるように)、コードの中を何度も上下(右往左往)しなくても容易に変数の役割・機能も理解できるようにコードは書くべきだとされている。[独自研究?]

そういった、現代では当たり前になっている手法を実践できていないような、低品質なプログラムがスパゲティプログラムである。

スパゲティプログラムではないもの

[編集]

単にプログラム規模に見合っていてソースファイルの数が多かったりバイナリ(実行ファイル)のサイズが相応に大きかったりするだけのプログラムや、リバースエンジニアリング防止のためにソースコード解析を意図的に困難にしたプログラムなどは、「スパゲティプログラム」には分類されない。

たとえば動的なウェブページの表示に必要とされるJavaScriptで書かれたプログラムの中には、コメントもなく、スペースや改行なしで変数名なども極端に短く、可読性を欠いたようなコードとなっているものもあるが、これはファイルのダウンロードおよびウェブブラウザによるスクリプト解釈の高速化を目的としたミニファイ(圧縮)処理や、ソースコードを解析しづらくする難読化を行うプログラミングツールによって自動生成されたコードであり、変換元のソースコードがそのようになっているわけではない。人間が読むことを目的としていない、自動生成されたコードをスパゲティコードと呼ぶことはない。

スパゲティプログラムの要因

[編集]

goto文の濫用

[編集]

スパゲティプログラムを作り出す原因としてよく挙げられるのが、goto文の濫用である。(構造化以前の)BASICなどの言語にあるgoto文は、機械語やアセンブリ言語のアドレス指定ジャンプ命令に近い特性を持ち、無条件に指定した行番号の位置までジャンプする。これはサブルーチン(関数)やループなどの制御構文を利用した制御に比べ、処理の(素直な、上から下への)流れを混乱させる。離れた位置に書かれた行へ飛ばせるので、もちろんソースコード可読性も低下させ、不具合や欠陥(バグ)を含んだコードを書いてしまう原因にもなる。

特に構造化されていないBASICは、各行に「行番号」があり、その行番号をジャンプ先として指定してジャンプするという原始的な機能だった。BASICにおいて、goto文はプログラム中のあらゆるところで登場し、プログラムがif文で条件分岐する際、goto文を使ってジャンプするということもよくあった。return文を持つサブルーチン機能もあるものの、GOSUBでサブルーチンに飛ぶためにはやはり行番号を指定する必要があり、goto文でサブルーチンの途中に飛ぶような危険なコードも記述可能だった。結果として、低品質なスパゲティプログラムが氾濫し、人々を悩まし、構造化以前のBASICは酷評されるようになり、BASIC自体が廃れてしまった。のちに全く別系統の、行番号やGOTO文を廃して構造化プログラミングを可能とした「構造化BASIC」が登場した。構造化BASICの子孫は、Visual Basic for Applications (VBA) やVisual Basic .NET (VB.NET) といった形で生き残っており、2023年現在も実用途で使われ続けている。

構造化プログラミングを可能とする構文機能を備えたPascalC言語では、一応goto文もサポートされていたものの、BASICのような無条件のジャンプ命令ではなく、サブルーチン(プロシージャ/関数)を飛び越える大域ジャンプには使えないものだった[注釈 4]

さらにC++Javaといった後発言語では、例外処理や多重ループを抜けるためのラベル付きbreak文(制限されたgoto文)を用意し、基本的には悪しきgoto文は使用しないようになった。C#にもgoto文は用意されているが、多重ループの脱出にgoto文を用いることは推奨されておらず、メソッドとして抽出してreturn文を用いることが推奨されている[3]

なお次のような場合、goto文を使うことが容認される場合や使わざるを得ない場合もあった。

  • C言語等でエラー発生時の後始末(ヒープメモリ解放やファイルクローズなど)を記述する場合、goto文を使うことでエラー処理をまとめて書きやすくなることがある[注釈 5]
  • リソースが極度に制限された組み込み環境など、高水準言語が使えず、アセンブリ言語のような低水準言語を直接使わざるを得ないケース

グローバル変数の安直な使用

[編集]

現代のプログラミングでは、できる限りグローバル変数の使用は控え、ローカル変数を優先的に使用するべきだとされている。グローバル変数はサブルーチンを超えてアクセス可能であり、公開宣言すればプログラムのどこからでもアクセスできるようになり、また寿命も長い(プロセスと同じ生存期間を持つ)ため、大規模で複雑なプログラムになるにつれて管理が難しくなり、事故を起こす可能性が高くなる。グローバル変数は、変数の定義位置と、変数が実際に読み書きされる箇所が遠く離れがちであり、気付かないうちに内容が書き換えられてしまう可能性もある。また、グローバル変数はプロセス内の複数のスレッドで共有される資源であり、複数のスレッドから同時にアクセスされる可能性がある場合は、アトミック操作排他制御を適切に記述しなければならない。グローバル変数をむやみに多用すると、容易にスパゲティコードとなる[6]

クラスのメンバー変数(フィールド)に関しても、同じインスタンスのメンバー関数(メソッド)であればどこからでもアクセスできるため、使い方によってはグローバル変数と似たような問題を抱えることになる。JavaやC#には、C/C++のような名前空間スコープに直接定義できるグローバル変数はないが、クラスには静的フィールドを定義することができる。静的フィールドはグローバル変数とほぼ同様の性質を持ち、自クラス外への公開性に関するアクセス制限をかけることはできるものの、グローバル変数と同様の問題点を持つ。

Singleton パターンは、静的ローカル変数や静的フィールドを使って実装されることが多いが、やはりグローバル変数と同様の問題を抱えることになるため、むやみに多用すべきではない。

C/C++では、グローバル変数やクラスの静的メンバー変数の初期化順序は、1つの翻訳単位(ソースファイル)内では定義した順すなわち上から下に初期化されるが、異なる翻訳単位の間の初期化順序は不定である[7]。そのため、翻訳単位を超えたグローバル変数や静的メンバー変数の初期化順序に依存したコードを書くと未定義動作を引き起こす。これもまたスパゲティコードの一種である。

ローカル変数のみを使用するサブルーチンは入出力が明確になり、部品化・再利用・テストもしやすくなるが、グローバル変数のようなサブルーチンをまたいでアクセス可能な変数を使用するサブルーチンは入出力が不明瞭になり、部品化・再利用・テストをしにくくなる。

なお、ローカル変数であっても、ポインタによるアドレス渡しや参照渡しによって、複数のサブルーチンから参照されうる。引数として渡された複数のポインタや参照が、それぞれ別の変数(またはオーバーラップしないデータ領域)を参照していることを仮定する仕様のサブルーチンに、同じ変数(またはオーバーラップするデータ領域)へのポインタを渡すと、予期しない危険な動作を引き起こすこともある[8]。クラスのメンバー変数をメンバー関数の引数としてアドレス渡しや参照渡しする場合も、同様の問題が発生しうる。コピー代入演算子やムーブ代入演算子では、自己代入に備えなければならない[9]。ポインタや参照はメモリ上のオブジェクト(データ)の実体にエイリアス(別名)を与えるものであり、前述のような注意点に配慮せず不用意に使うと、グローバル変数のように思わぬタイミングで内容が書き換わってしまうこともあり、未定義動作やスパゲティコードの原因になることがある。

継承の濫用

[編集]

オブジェクト指向を取り入れたプログラミング言語においても、継承を機能追加のために濫用し、クラス間の関係が複雑になりすぎてしまうことでスパゲティ化が起こることがある。特に多重継承はメンバーの名前の衝突や菱形継承などの問題を抱えているため、多重継承はアンチパターンとされた。C++では仮想継承を使うことで菱形継承問題を回避できるが、オブジェクトサイズが肥大化するなどの別の問題もある。DelphiJavaC#などの言語ではインターフェイスの多重継承のみが許可され、実装の多重継承は禁止された。多重継承だけでなく単一継承による差分プログラミングも、拡張性や互換性を意識してスーパークラス(基底クラス)を慎重に設計する必要があり、むやみに使うとプログラムの複雑化やメンテナンス性の低下を招く。継承はgoto文と同じくらいプログラムを分かりにくくする要因であるという意見もある[10]。『Effective C++』や『Effective Java』のような書籍では、機能の追加には継承よりもオブジェクトコンポジション(合成)を利用することがベストプラクティスとして推奨されている。

不適切なマルチスレッドプログラミング

[編集]

並行処理並列処理のために複数のスレッドを使用するマルチスレッドプログラミングでは、処理の流れが1つではなく、同時に並行動作する複数のスレッドが互いに協調し合う必要があり、不用意にスレッドを使うとスパゲティ化を招きやすい。マルチスレッドによる非同期処理は、従来とはまったく別の意味でのスパゲティコードをもたらす[11]。スレッドを利用する際は、データ競合による未定義動作競合状態による意図しない動作が発生しないよう、スレッドセーフを意識して慎重にプログラミングする必要がある。排他制御の作法を誤ると性能低下やデッドロックのような問題も発生する。特にマルチスレッドのバグはタイミングによって異常が発生したりしなかったりすることもあるため、シングルスレッドのプログラミングよりもデバッグの難易度が高い。

マルチスレッドプログラミングを簡略化し、コードの信頼性を向上するために、構造化並行性 (structured concurrency) と呼ばれる概念についても議論されている[12]

コールバックの多用

[編集]

コールバックを多用するプログラム、特にイベント駆動型プログラミングもスパゲティコードを招きやすい。例えばGUIアプリケーションは常にユーザー操作に対する応答ができるようになっていることが重要であり、イベントループを持ちユーザー応答をつかさどるメインスレッドでネットワーク通信やストレージI/Oなどの長時間かかる可能性のある処理をその場で同期的に実行するとアプリケーションが応答停止(フリーズ)することがある。そのため、いったん他のスレッドやプロセスに実際の処理を任せるようにリクエストを発行した後、処理結果をコールバック関数の引数などの形で非同期的に受け取るようなイベント通知スタイルを採用する必要があるが、その結果を受けて次に実行する処理をさらにコールバック関数で記述して……といったように、非同期処理のコードは一連の流れを把握しづらいスパゲティスタックとなりやすい[13]。この問題を緩和するために、Futureに対応したライブラリや、それを発展させたasync/await構文をサポートする言語なども登場している。

動的結合の濫用

[編集]

プログラムのカスタマイズポイントを提供するコールバック関数、オブジェクト指向のポリモーフィズム(多態性)を実現する仮想関数、ダック・タイピングに使われるリフレクションのような動的結合(動的ディスパッチ英語版または動的バインディング)は、アルゴリズムの再利用性向上のために有用な機能だが、実行時でなければ実体の特定(名前解決)ができず、統合開発環境の機能を使っても呼び出し構造や依存関係を直接追跡できないため、濫用するとスパゲティコードを招きやすい[14]。関数オーバーロードや演算子オーバーロード、C++テンプレートのような静的結合であっても、テンプレートメタプログラミングなどで濫用するとスパゲティコードを招きやすくなることもある。

しかたなくスパゲティプログラムにしてしまった事例

[編集]

現在ではスパゲティプログラムは絶対に書いてはいけないとされている。現在ではメモリ量はふんだんにあり、可読性が重要で、メンテナンスやコードの改変が容易で、システムが安定して動作することが重要だということが理解されているからである。

だが特に1980年代ころまでの、まだ構造化されていないプログラミング言語が横行していていた時代で、構造化前のプログラミング言語しか動かないマシンで限られたメモリにプログラムをおさめなければならなかった状況下では、しかたなくGoto文が使われることがあった。数バイトでも削減したいような、追い詰められた開発環境では、無理やり詰め込むため、現代から見れば「あり得ない」「許容されない」ような、粗野なプログラミング手法ですら使われた。

一部のシステムでは、CPUのバグを突くようなトリッキーなプログラミング手法がしかたなく選ばれていたものがあり、ソースコードやアセンブリコードを普通の読み方で読んだだけではとても理解できないようなジャンプが起き、まさにプログラムのスパゲティ化を招いた。例えばファミコンApple IIなどのゲームにおいては、65C02の仕様書に記載されていない未定義命令(Undocumented instruction、本来使ってはいけない)を利用することが常套化していた。特にファミコン後期においては、競合機や次世代機の登場によってファミコンのゲームに対する性能への要求が強くなり、また視覚的キャラクタに割り当てられるデータ量・メモリ量の増大によってプログラムのほうに割り当てられるバイト数が減ってしまい、本来してはいけないはずのコーディング手法が、ますます横行するようになってしまった。たとえばファミコン用ゲーム『ファイナルファンタジーIII』の飛行艇の高速移動のプログラミングに関しては、既に退職した当時のメインプログラマのナーシャ・ジベリ以外誰も理解できず、そのため当時の人気ゲームでありながら後継機への移植が難航したという逸話がある。[要出典]

スパゲティプログラムが許容、放置された具体例

[編集]

1980年代のメモリ量が限られていた時代には、プログラムのバイト数を圧縮する技を競い合う誌上コンテンストなどが開催されていて、そこではスパゲティプログラムが許容された。例えば、雑誌MSX・FANの投稿プログラムコーナー『ファンダム』で事実上のスパゲティプログラムが許容されてしまっていた。特に「1画面プログラム」すなわち行番号を含めた40×24文字のショートプログラムを多数採用・掲載しており、行から行へと、標準的ではない手法でジャンプをさせるようなコードが横行した。

たとえば次のようなものである。

  • 行内の分岐に関わるIF文の使用を極力避けてしまう (if文を終了するには行を終了するしか方法が無かった為)
  • NEXTの対象変数を書かない

たとえばBASICにおけるトリガー入力待機ルーチンは通常

10 IF STRIG(0)=0 THEN GOTO 10

と記述するのに対し、1画面プログラム採用作品では

10 FORI=0TO1:I=-STRIG(0):NEXT

という記述が用いられていた。

中には「マシン語ソースをアスキーコード制御文字列を含まない範囲の8ビット値にシフトさせ、そのキャラクターコード群とデコーダのみ記述する」という可読性が全く無いプログラムが採用される事もあった。

別の例

「*」をコントローラーで左右に動かすプログラムを一般的な記法で書いたもの。
条件分岐させ水平座標、Xが 0未満、28を超えないように加減算する。

10 CLS
20 LET X=14
30 LOCATE X,10:PRINT " ";
40 LET S=STICK(0)
50 IF S=3 THEN X=X+1: IF X>28 THEN X=28
60 IF S=7 THEN X=X-1: IF X<0 THEN X=0
70 LOCATE X,10:PRINT "*";
80 GOTO 30

以下はよく用いられていた圧縮方法で書かれたソース。

1 CLS:X=14
2 LOCATE X,10:PRINT" ";:S=STICK(0):X=X-(S=3)*(X<28)+(S=7)*(X>0):LOCATE X,10:PRINT"*";:GOTO 2

メイン部分を1行にまとめるために、条件分岐を省いている。
水平座標の変数 Xのはみ出しをチェックする関係式の評価を数値として扱い、1個の算術代入文としている。
しかしMSX BASICでは式の評価が 真ならば-1、偽ならば0 が返る為、一見すると変数 Xへの加算と減算が逆に見えてしまう。

その他

また、五・七・五・七・七の三十一バイト(みそひとバイト) でプログラムを記述するという曲芸的ジャンル「アセンブラ短歌」を考案した人も昔はおり、こうした作品でも31バイトの中でプログラムを完結させるためにスパゲティ状のジャンプを内部で行うものが多々あった。

スパゲティプログラムを修正する方法

[編集]

スパゲティプログラムは保守や機能追加を妨げるので、できることなら修正することが望ましい。しかし実務で使われているシステムは「スパゲティプログラムを修正した場合のメリットとデメリット」「修正せず、そのまま放置する場合のメリットとデメリット」を天秤にかけて、「とりあえずうまく動作しているプログラムは、滅多なことでは修正しない」ということが広く行われている。

各種の汎用オペレーティングシステム、ソフトウェア開発ツール、金融機関の基幹システム、産業用機械の制御ソフトウェア、業務用アプリケーションなど、実務で日々使用されているシステムでは安定性が非常に重要であり、不用意にコードを修正してうっかりシステムの安定性を損ねるとシステムに依存した業務が停止してしまい、ユーザーに多大な迷惑をかけるだけでなく、不具合によって発生した金銭的な損失に対して補償しなければならなかったり、果てはユーザーを失なったりする事態に陥るからである。

またスパゲティプログラムを修正するとしても、十分な解析やテストをせずにうかつに修正してしまうと、かえって既存機能や動作の互換性を損なったり、別のバグを追加してしまったり、修正されていたはずのバグを復活[注釈 6]させてしまったりする可能性が高いからである。時間や予算・人材が許す場合でも、この傾向は見られた。スパゲティプログラムを修正するとしても、コードが入り組んでいて解読や分割・分離が難しいことから、しばしば小手先の作業だけでは困難で、相当に大掛かりな作業になることが多い。

後にテストファーストの方法論が確立され、プログラム本体の完成と同時期にテストプログラムも作成されるようになると、プログラム変更の危険性は相対的に低くなり、不適切な状態のプログラムは積極的に修正することが奨励されるようになった(リファクタリング)。

なおあまりに酷い状態に陥っているスパゲティプログラムは、修正するのではなく、思い切って放棄してしまって、新たにゼロから整然と構造化したプログラムを書いたほうがよほど早い、ということもある。

脚注

[編集]

注釈

[編集]
  1. ^ 1命令や1行だけを実行させ、命令ごとあるいは行ごとの状態が正常かどうかひとつひとつ確認すること。
  2. ^ 大規模プロジェクトでは命名規則がコーディング規約で整備されていることが多いが、その命名規則に従っていない一貫性のないコードは可読性の低いプログラムになりやすい。そもそも命名規則自体が現代的なコーディングスタイルに則しておらず不適切であることもある。
  3. ^ 1つの変数に複数の意味・役割を持たせて使いまわしすると、変数名も不適切・あいまいになりやすく、コードの可読性やメンテナンス性が低下する。
  4. ^ ただしCには大域ジャンプを可能とするsetjmp()longjmp()も用意されていた。
  5. ^ C++やObject Pascalにはデストラクタがあり、C#やJavaではusing文[4]やtry-finally文やtry-with-resources文[5]が使えるため、確実なリソース解放のためにgoto文やラベル付きbreak文などを使用する必要はない。
  6. ^ 「寝たバグを起こす」「寝ているバグを起こす」とも形容される。

出典

[編集]

関連項目

[編集]