スパゲティプログラム
出典: フリー百科事典『ウィキペディア(Wikipedia)』 (2023/11/04 02:40 UTC 版)
スパゲティプログラムの要因
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年現在も実用途で使われ続けている。
構造化プログラミングを可能とする構文機能を備えたPascalやC言語では、一応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++では仮想継承を使うことで菱形継承問題を回避できるが、オブジェクトサイズが肥大化するなどの別の問題もある。Delphi、Java、C#などの言語ではインターフェイスの多重継承のみが許可され、実装の多重継承は禁止された。多重継承だけでなく単一継承による差分プログラミングも、拡張性や互換性を意識してスーパークラス(基底クラス)を慎重に設計する必要があり、むやみに使うとプログラムの複雑化やメンテナンス性の低下を招く。継承はgoto文と同じくらいプログラムを分かりにくくする要因であるという意見もある[10]。『Effective C++』や『Effective Java』のような書籍では、機能の追加には継承よりもオブジェクトコンポジション(合成)を利用することがベストプラクティスとして推奨されている。
不適切なマルチスレッドプログラミング
並行処理や並列処理のために複数のスレッドを使用するマルチスレッドプログラミングでは、処理の流れが1つではなく、同時に並行動作する複数のスレッドが互いに協調し合う必要があり、不用意にスレッドを使うとスパゲティ化を招きやすい。マルチスレッドによる非同期処理は、従来とはまったく別の意味でのスパゲティコードをもたらす[11]。スレッドを利用する際は、データ競合による未定義動作や競合状態による意図しない動作が発生しないよう、スレッドセーフを意識して慎重にプログラミングする必要がある。排他制御の作法を誤ると性能低下やデッドロックのような問題も発生する。特にマルチスレッドのバグはタイミングによって異常が発生したりしなかったりすることもあるため、シングルスレッドのプログラミングよりもデバッグの難易度が高い。
マルチスレッドプログラミングを簡略化し、コードの信頼性を向上するために、構造化並行性 (structured concurrency) と呼ばれる概念についても議論されている[12]。
コールバックの多用
コールバックを多用するプログラム、特にイベント駆動型プログラミングもスパゲティコードを招きやすい。例えばGUIアプリケーションは常にユーザー操作に対する応答ができるようになっていることが重要であり、イベントループを持ちユーザー応答をつかさどるメインスレッドでネットワーク通信やストレージI/Oなどの長時間かかる可能性のある処理をその場で同期的に実行するとアプリケーションが応答停止(フリーズ)することがある。そのため、いったん他のスレッドやプロセスに実際の処理を任せるようにリクエストを発行した後、処理結果をコールバック関数の引数などの形で非同期的に受け取るようなイベント通知スタイルを採用する必要があるが、その結果を受けて次に実行する処理をさらにコールバック関数で記述して……といったように、非同期処理のコードは一連の流れを把握しづらいスパゲティスタックとなりやすい[13]。この問題を緩和するために、Future パターンに対応したライブラリや、それを発展させたasync/await構文をサポートする言語なども登場している。
動的結合の濫用
プログラムのカスタマイズポイントを提供するコールバック関数、オブジェクト指向のポリモーフィズム(多態性)を実現する仮想関数、ダック・タイピングに使われるリフレクションのような動的結合(動的ディスパッチまたは動的バインディング)は、アルゴリズムの再利用性向上のために有用な機能だが、実行時でなければ実体の特定(名前解決)ができず、統合開発環境の機能を使っても呼び出し構造や依存関係を直接追跡できないため、濫用するとスパゲティコードを招きやすい[14]。関数オーバーロードや演算子オーバーロード、C++のテンプレートのような静的結合であっても、テンプレートメタプログラミングなどで濫用するとスパゲティコードを招きやすくなることもある。
注釈
- ^ 1命令や1行だけを実行させ、命令ごとあるいは行ごとの状態が正常かどうかひとつひとつ確認すること。
- ^ 大規模プロジェクトでは命名規則がコーディング規約で整備されていることが多いが、その命名規則に従っていない一貫性のないコードは可読性の低いプログラムになりやすい。そもそも命名規則自体が現代的なコーディングスタイルに則しておらず不適切であることもある。
- ^ 1つの変数に複数の意味・役割を持たせて使いまわしすると、変数名も不適切・あいまいになりやすく、コードの可読性やメンテナンス性が低下する。
- ^ ただしCには大域ジャンプを可能とする
setjmp()
とlongjmp()
も用意されていた。 - ^ C++やObject Pascalにはデストラクタがあり、C#やJavaではusing文[4]やtry-finally文やtry-with-resources文[5]が使えるため、確実なリソース解放のためにgoto文やラベル付きbreak文などを使用する必要はない。
- ^ 「寝たバグを起こす」「寝ているバグを起こす」とも形容される。
出典
- ^ スパゲッティコード(スパゲッティプログラム)とは - 意味をわかりやすく - IT用語辞典 e-Words
- ^ スパゲッティコードの意味とは?具体例や対策について詳しく解説
- ^ ジャンプ ステートメント - break、continue、return、goto - C# | Microsoft Learn
- ^ using ステートメント - 破棄可能なオブジェクトが正しく使用されるようにする - C# | Microsoft Learn
- ^ try-with-resources 文 | Java SE 7 Documentation | Oracle
- ^ アンチパターンってなに? | Think IT(シンクイット)
- ^ 初期化 - cppreference.com
- ^ memcpy, memcpy_s - cppreference.com
- ^ How to: Define move constructors and move assignment operators (C++) | Microsoft Learn
- ^ Opinion -- 川俣 晶:ソフト開発を成功させる1つの方法 - @IT
- ^ Lecture 4: IPC & Threads / CSE 120: Principles of Operating Systems | Alex C. Snoeren, カリフォルニア大学サンディエゴ校
- ^ JEP 428: javaマルチスレッドプログラミングを容易にする構造化並行性
- ^ Windows with C++ - The Pursuit of Efficient and Composable Asynchronous Systems | Microsoft Learn
- ^ まずコードの可読性を最適化しよう | POSTD
固有名詞の分類
プログラミング言語の構文 |
ハンガリアン記法 Lint スパゲティプログラム 配列 字下げスタイル |
- スパゲティプログラムのページへのリンク