コードの悪臭:その正体と技術的負債との関連性

コードの悪臭:その正体と技術的負債との関連性

コードの臭いはバグではありません。バグのあるプログラムはクラッシュしたり、誤った結果を返したり、テストに失敗したりします。コードの臭いがあるプログラムは何年も問題なく動作するかもしれませんが、変更を加えるたびにコストがかさみ、新機能を追加するたびに予期せぬリスクが生じ、リファクタリングを試みるたびに誰も知らなかった依存関係が明らかになります。コードの臭いは、将来の問題を予測するコードの構造的特徴です。すぐに障害を引き起こすわけではありませんが、将来のあらゆる変更を必要以上に困難、遅延、そして危険なものにします。

この用語は、マーティン・ファウラーとケント・ベックがファウラーの著書『リファクタリング:既存コードの設計改善』(1999年)で広めたもので、同書では22種類のコードスメルが列挙され、それぞれに対応するリファクタリング手法が示されました。このカタログは今でも標準的な参考資料となっており、ファウラーが名付けた「長いメソッド」「神クラス」「重複コード」「機能への羨望」「分岐変更」「ショットガン手術」などのコードスメルは、現在でも業界全体のSonarQubeルールセット、静的解析ツール、コードレビューチェックリストに登場しています。

コードの臭いを消す

SMART TS XL 複雑なシステム全体でそれらをマッピングして修正するのに役立ちます。

詳細情報

コードの悪臭とは何ですか?

コードの臭いとは、ソースコードの表面的な特徴であり、より深い構造的または設計上の問題を示唆するものです。コードはコンパイルされ、テストに合格し、正しい出力を生成しますが、その構造上の何らかの問題により、本来よりも読みにくく、拡張しにくく、安全に修正することが困難になっています。ファウラーの定義は、「通常、システム内のより深い問題に対応する表面的な兆候」です。

コードの臭いは、構文エラーやアサーションの失敗といった意味での違反ではありません。それらは、たとえ目に見える不具合がなくても、経験豊富な開発者が警告サインとして認識する指標、つまりパターンです。危険なのは、それらが累積していくことです。1万行のコードベースの中に長いメソッドが1つあるだけでは、些細な不便さで済みます。しかし、何百もの長いメソッド、数十のモジュールにまたがる重複したロジック、そして依存関係グラフの中心にある巨大なクラスといったものが、安全な変更を真に困難にするシステムになってしまうのです。

コードの悪臭 vs. バグ vs. 技術的負債

これら3つの概念は関連しているものの、それぞれ異なるものであり、混同すると優先順位付けが不適切になる。

概念 即座に失敗する?見つけ方
バグ誤った動作を引き起こすコードはい、テストは失敗し、ユーザーはエラーを報告します。テスト、監視、エラーログ
コードの臭い将来の問題を予測する構造パターンいいえ、コードは正しく実行されますコードレビュー、静的解析
技術的負債過去の近道や誤った判断の累積コストいいえ、しかし時間の経過とともに複利的に指標、複雑性分析、リファクタリング工数見積もり

コードの悪臭は、技術的負債が蓄積されるメカニズムです。コードベースに追加される長いメソッドは、それぞれ技術的負債の単位であり、その利息は、将来のすべての開発者がそのメソッドを理解するために費やす余分な時間と、将来のすべての変更がそのメソッドのサイズによる副作用を回避するために費やす時間です。

SonarQubeにおけるコードスメルとは何ですか?

SonarQubeは、コードの問題をバグ(明らかに間違っているコード)、脆弱性(セキュリティ上の問題)、コードスメル(保守性の問題)の3つのカテゴリに分類します。SonarQubeのコードスメルは、Fowlerの分類体系に直接対応しており、長いメソッド(設定可能な行数のしきい値を超えるもの)、重複したブロック、パラメータが多すぎるもの、認知複雑度スコアが高いもの、エラー処理が欠落しているもの、アーキテクチャ上の結合違反などに関するルールが含まれています。SonarQubeのコードスメルルールは、Fowlerのオリジナルの分類体系を自動化して運用する、業界で最も広く利用されている手法です。

マーティン・ファウラー著『コードの臭い:古典的な分類法』

ファウラーが最初に提唱した、カテゴリ別に整理された22種類のコードスメルは、現在でも標準的な参照基準となっている。主要な静的解析ツールのルールセットはすべて、この分類法に基づいている。

カテゴリーコードの臭い
ブロウター扱いにくいほど大きくなったコード長いメソッド、大きなクラス、プリミティブ型への執着、長いパラメータリスト、データの塊
オブジェクト指向を悪用する人々オブジェクト指向原則の誤用switch文、一時フィールド、拒否された継承、異なるインターフェースを持つ代替クラス
変化の阻害者変化を困難にする分岐的な変化、ショットガン手術、並列継承階層
消耗品不要なコードコメント(過剰)、重複コード、遅延クラス、データクラス、デッドコード、投機的一般性
カプラ過剰な結合機能への羨望、不適切な親密さ、メッセージの連鎖、仲介者

臭いがどのカテゴリに属する​​かを理解することで、修復の優先順位付けに役立ちます。膨張する臭いと変更を阻害する臭いは、リファクタリングコストの高さと直接相関しています。結合する臭いは、アーキテクチャの脆弱性と直接相関しています。不要な臭いは、最も安全に削除できます。

よくあるコードの臭い:クイックリファレンス

コードの臭いそれはどのようなものか主なリスク
重複コード同じロジックが複数の箇所に現れるバグ修正はあらゆる箇所に適用する必要がある。コピーは時間の経過とともに乖離していく。
ロングメソッド複数の責任を伴う、20~30行を超えるメソッド認知負荷が高い。個々の行動をテストするのが難しい。
神クラス/大規模クラスすべてをこなすクラスすべての機能変更が同じクラスに影響を与えるため、マージの競合や脆弱性が発生する可能性があります。
長いパラメータリスト4つ以上のパラメータを取るメソッド誤った値を渡しやすい。呼び出し箇所が読みにくい。
機能への憧れ自身のデータよりも他のクラスのデータを多く使用するメソッド密接な結合。一方のクラスの変化がもう一方のクラスを壊す。
分岐変化さまざまな理由で変更されたクラスが1つあります。単独責任の原則に違反する。予測不可能な副作用がある。
ショットガン手術1つの変更で、多くのクラスにわたる編集が必要になります。変更コストが高い。インスタンスを見落としやすい。
デッドコード決して呼び出されない、または到達しないコード開発者を混乱させる。長年にわたって蓄積される。移行を複雑にする。
原始的な執着ドメインオブジェクトの代わりに基本型(文字列、整数)を使用する検証が至る所に散在している。表現力に乏しい。
データ塊同じフィールド群が繰り返し一緒に渡されるドメインオブジェクトであるべき。抽象化が欠けていることを示している。
推測的一般性将来のニーズを想定して書かれたコード不必要な複雑さ。なぜそれがそこにあるのか誰も理解していない。
一貫性のないエラー処理サイレントキャッチ、さまざまな例外戦略障害が検出されないため、デバッグに非常に時間がかかる。

コードの悪臭の定義と例

重複コード

大規模システムにおいて最も一般的で、最もコストのかかるコードの悪臭。重複は、コピー&ペーストによる開発、時間的制約、そして同じ問題をそれぞれ独立して解決しようとするチームが孤立して作業することによって発生する。その直接的な結果として、保守コストが増大する。共有ロジックへの変更はすべて、すべてのコピーに適用する必要があるからだ。

ジャワ

// ServiceA -- discount calculation
double calculateDiscount(double amount) {
    if (amount > 1000) return amount * 0.1;
    return 0;
}

// ServiceB -- same logic, copied and forgotten
double computeDiscount(double value) {
    if (value > 1000) return value * 0.1;
    return 0;
}

ビジネスルールが変更されると(しきい値が1500、レートが12%になるなど)、一方のコピーは更新されますが、もう一方は更新されません。その結果、2つのモジュール間で基本的なビジネスロジックに矛盾が生じ、テスト段階ではなく、監査時に本番環境でその不一致が顕在化します。

修正方法:共通のロジックを、両方の呼び出し元が参照する単一の関数、ユーティリティクラス、または共有ライブラリに抽出します。

ロングメソッド

時間の経過とともに新たな責任を吸収し、本来の目的を超えて発展してきたメソッド。200行のメソッドを読む際の認知負荷は、10行のメソッドを20個読む場合とは、量的な違いだけでなく質的な違いもある。長いメソッドは、個別にテストするには処理が多すぎるためテストが難しく、また、実行コンテキスト全体をワーキングメモリに保持する必要があるため理解も難しい。

検出閾値:20~30行を超えるメソッドはレビューが必要であり、50行を超える場合はほぼ確実にリファクタリングが正当化されます。COBOLでは、100文を超える段落がこれに相当します。

パイソン

class OrderProcessor:
    def process_order(self, order):
        # Validate order -- 40 lines
        # Calculate discounts -- 30 lines
        # Update inventory -- 25 lines
        # Send notification emails -- 20 lines
        # Generate invoice -- 35 lines
        # 150+ lines total
        pass

このメソッドにおける各責任は、それぞれ独立したクラスまたは関数として定義されるべきです。それらをまとめて管理すると、請求書発行、在庫管理、通知などの今後の更新のたびに、注文処理フロー全体が不安定になるリスクが生じます。

神級

複数のドメインにまたがる責任を蓄積し、単一責任の原則に著しく違反しているクラス。その結果、コードベースの重心となり、すべてがそれに依存し、その中の何かを変更するには、それに関するすべてを理解する必要が生じる。

検出シグナル:20~30個以上の公開メソッドを持つクラス、または名前に「Manager」、「Processor」、「Handler」、「Utils」、「Helper」が含まれるクラスが、複数の無関係なドメインに適用されている場合。

分岐変化

さまざまな、互いに関連性のない理由で変更されるクラス。データベーススキーマが変更されるたびに、このクラスを編集する必要があります。価格設定ルールが変更されるたびに、このクラスを編集する必要があります。通知フォーマットが変更されるたびに、このクラスを編集する必要があります。このクラスは責任が多すぎるため、分割する必要があります。

定義:様々な理由で常に変化し続けるクラス。ショットガン手術の逆。

ショットガン手術

概念的な変更が一つあるだけで、多くの異なるクラスにわたる編集が必要になります。税率を変更するには、バックエンドの計算、フロントエンドの検証、データベースのトリガー、バッチジョブ、レポートクエリの5つの異なる箇所を修正する必要があります。どれか一つでも見落とすと、動作が不安定になります。

SQL

-- Tax logic duplicated across queries
SELECT amount * 0.05 FROM invoices;
SELECT amount * 0.05 FROM payments;
SELECT amount * 0.05 FROM reports;

0.05を0.07に変更するには、SQLファイル、ストアドプロシージャ、およびアプリケーションコード全体にわたって、その変更箇所をすべて見つけ出す必要があります。

機能への憧れ

自身のデータやメソッドよりも、別のクラスのデータやメソッドを多く使用するメソッド。これは、その動作が本来は別のクラスに属するべきであることを示唆しています。

ジャワ

// In ReportGenerator -- envious of Customer's data
double calculateCustomerRating(Customer customer) {
    return customer.getOrderCount() * customer.getAverageOrderValue()
           / customer.getDaysSinceRegistration();
}
// This logic belongs in Customer, not ReportGenerator

デッドコード

リポジトリには存在するものの、本番環境ではどの実行パスからも呼び出されないコード。デッドコードは、機能が削除、置換、または再構築される際に古いコードが削除されないまま放置されると、長年にわたって蓄積されます。コードレビューの妨げとなり、開発者がコードベースに慣れる際の混乱を招き、移行分析を複雑化させ、場合によっては意図せず再アクティブ化されることもあります。

検出: SonarQube、Knip (TypeScript/JavaScript用)、および SMART TS XL コードベース全体で、到達不能な関数、呼び出されていないメソッド、および未使用の変数を特定します。

DRY原則違反

DRY(Don't Repeat Yourself)原則とは、システム内のあらゆる知識は、単一の明確な表現でなければならないという原則です。DRY原則に違反すると、コードの重複、データの塊、そして多くの「ショットガン・サージェリー」シナリオの根本原因となります。ビジネスロジックが複数の場所に表現されている場合、それらの表現は必然的に乖離します。DRYは原則であり、コードの重複はその違反を示す兆候です。

パイソン

# DRY violation: same validation logic in three places
def validate_email_in_registration(email):
    return "@" in email and "." in email

def validate_email_in_profile_update(email):
    return "@" in email and "." in email

def validate_email_in_checkout(email):
    return "@" in email and "." in email

# DRY-compliant: one function, three callers
def is_valid_email(email):
    return "@" in email and "." in email

検出閾値:コードはいつ「臭い」となるのか?

コードの臭いを検出するには、測定可能な閾値が必要です。以下に、一般的に使用される指標と、注意が必要な臭いを示す値を示します。

メトリック測定対象警告しきい値クリティカルしきい値
循環的複雑性メソッド内の決定分岐の数10上記20上記
メソッドの長さ(行数)メソッド/関数内の行数20上記50上記
パラメータ数メソッドが受け入れるパラメータの数4上記7上記
授業時間クラス内の行数200上記500上記
重複率重複しているコードの割合3%以上10%以上
認知複雑性コードの理解の難しさ15上記25上記
求心性結合(Ca)このクラスに依存するクラスの数15上記30上記
遠心性結合(Ce)このクラスが依存するクラスの数15上記30上記

これらのしきい値はSonarQubeで設定可能であり、ほとんどの静的解析プラットフォームでは、これらの指標に基づいたカスタムルールを設定できます。クリティカルしきい値に該当するクラスとメソッドは、リファクタリングの優先順位が最も高い対象です。これらは将来的に不具合が発生する可能性が最も高く、保守コストも最も高いコンポーネントだからです。

コードスメル検出ツール

大規模なコードベース全体にわたってコードの不具合を特定するには、自動検出が唯一の拡張可能なアプローチです。手動レビューでは、自動ツールが検出できる不具合のごく一部しか検出できず、数百万行ものコードを含むレガシーシステムには対応できません。

ツール第一言語検出するもの
SonarQube / SonarCloudJava、Python、JS/TS、C#などファウラーの完全な匂い分類、セキュリティ上のホットスポット、重複
Checkstyle + PMDJavaスタイル違反、重複、複雑性指標
ESLint + typescript-eslintJavaScript、TypeScript長い関数、複雑性、未使用コード
ピリント+ラドンPython 複雑性、スタイル、保守性指標
リシャーパー/ライダーC#冗長なコード、長いメソッド、結合の問題
クリピRustRustにおける慣用表現の違反、コードの臭いとなる一般的なパターン
コードクライメート多言語複雑性、重複性、保守性スコア
SMART TS XLCOBOL、JCL、Java、Python、RPG、SQL、.NET言語間の重複、デッドコード、結合、依存関係のずれ

Rustにおけるコードの臭い これらは主にClippyによって検出され、慣用的なRustパターンを強制します。最も一般的なRust特有の臭いには、不必要なクローン、 unwrap() 生産パス、過度にネストされたマッチ式、および値を返す関数 Result しかし、代わりにパニックを使用してください。

コードの悪臭と技術的負債:その関連性

技術的負債とは、品質よりもスピードを優先した過去の意思決定によって蓄積されたコストのことです。コードの臭いは、その負債がコード構造に現れるメカニズムです。両者の関係は直接的で、対処されていないコードの臭いは技術的負債の単位であり、その利息は、将来の変更で回避しなければならない余分な時間です。

ソフトウェア変更管理における影響分析の文脈で説明されているように、コードの臭いが示す構造的な問題、つまり過剰な結合、重複したロジック、デッドコードの蓄積は、特定の変更が何に影響を与えるかを特定することを困難にするため、あらゆる変更の範囲を直接的に拡大します。

技術的負債をコードの悪臭という観点から説明しましょう。コードベースに40%の重複がある場合、バグ修正にかかるコストは本来の1.4倍になります。コア処理クラスがすべての機能の依存元となる「神クラス」である場合、機能を追加するたびにクラス全体を理解し、テストする必要があります。エラー処理に一貫性がない場合、障害シグナルが信頼できないため、本番環境で発生するインシデントごとに調査時間が長くなります。技術的負債は抽象的なものではなく、こうした非効率性が積み重なって生じるものです。

CISQの調査によると、開発者は新しい機能の開発よりも、技術的負債の回避に時間の30~40%を費やしていることが一貫して明らかになっています。コードの臭いの密度は、どれだけの負債が蓄積されているかを最も直接的に測定する指標です。

認定条件 SMART TS XL エンタープライズ規模でコードの悪臭を検出します

SonarQubeやClippyといった個々のツールは、単一の言語内で動作します。しかし、COBOLプログラムがJavaサービスが読み込むデータセットに書き込みを行い、JCLジョブストリームが複数の言語で書かれたプログラムを呼び出し、同じビジネスロジックが3つの異なる年代に書かれた3つの異なるシステムにそれぞれ独立して複製されているようなエンタープライズ環境では、単一言語ツールでは全体像を把握できません。

SMART TS XLさん 静的コード分析 環境内のすべての言語にわたるコードの臭いを同時に検出します。例えば、COBOL コピーブックと Java ユーティリティ クラス間の重複したロジック、JCL ジョブが呼び出さない RPG プログラムのデッド コード、1 つの段落で 50 の作業を行う COBOL プログラムのゴッド クラス パターン、言語境界を越えた一貫性のないエラー処理パターンなどです。

アプリケーションの依存関係マッピング機能は、個々のファイルレベルのツールでは検出できないアーキテクチャ上の問題点を特定します。例えば、最も高い結合度(最も依存度が高く、変更時に破損するリスクが最も高い)を持つコンポーネント、独立しているべきモジュール間に循環依存関係が存在する箇所、重複したビジネスロジックが異なるシステムで独立して維持されているにもかかわらず、どちらのコピーも互いの存在を認識していない箇所などです。

影響分析機能により、コードの不具合を具体的な対策に結びつけることができます。結合度の高いコンポーネントをリファクタリングする前に、影響分析によってテスト、検証、または更新が必要な依存コンポーネントがすべて列挙されます。これにより、大規模でコードの不具合が多いコードベースでチームが経験する「リファクタリング麻痺」が解消され、各変更に明確な範囲が設定された、構造化された是正プログラムへと移行します。リスクが不明瞭なままでは、変更内容が明確に定義され、リスクも不明瞭なままではなくなります。

レガシーシステムの近代化プログラムを実施するチームにとって、コードスメル分析は近代化計画の基盤となります。移行を開始する前にデッドコードを削除し(範囲を縮小)、重複したロジックを標準的な実装に統合し、最も結合度の高いコンポーネントを最後に近代化し(それらに依存するすべてのものが対処された後)、ゴッドクラスを新しい言語に変換する前に分解します。なぜなら、ゴッドクラスをJavaに変換すると、Javaでもゴッドクラスが生成されるからです。

コードの悪臭に対処する:優先順位付けフレームワーク

すべてのコードの悪臭が即座にリファクタリングを必要とするわけではありません。適切なアプローチは、リスクに基づいた優先順位付けです。

優先度1:変更頻度の高いコンポーネントにおけるコードの不具合。頻繁に変更され、複雑性や結合度が高いコードは、最も多くの不具合を発生させます。これらのコンポーネントは変更コストが最も高く、本番環境でのインシデント発生件数も最も多くなります。まずはこれらの不具合を修正してください。

優先度2:アーキテクチャ境界における問題点。すべての要素が依存する、いわゆる「神クラス」や高結合コンポーネントは、変更すると最も危険であると同時に、修正せずに放置すると最もコストがかかる。これらのリファクタリングを行う前に、最も慎重な影響分析が必要となる。

優先度3:システム境界をまたいだコードの重複。同じビジネスロジックが複数のシステムに存在する場合、変更はすべてのコピー間で同時に調整する必要があります。この重複を統合することで、調整のオーバーヘッドが削減され、乖離を防ぐことができます。

優先度4:不要コードの削除。不要コードは最も安全に対処できるカテゴリです。削除しても動作が損なわれることはなく、これまで隠されていた依存関係が明らかになるだけです。呼び出されることのないコードを変換する無駄な労力を避けるため、移行や変換を行う前に削除する必要があります。

優先度5:リスクの低い領域におけるスタイルと構造上の問題点。安定していて変更頻度の低いコード内の長いメソッドやパラメータリストは、近くのコードが他の理由で変更する必要が生じた際に、機会を捉えて対処し、同時に周囲の問題点もリファクタリングすることができます。

コードの不具合を、それが既に本番環境での障害を引き起こしてから事後的に対処するのではなく、体系的に検出、測定、対処する規律こそが、開発チームが長期にわたって開発速度を維持できるかどうか、そしてシステムが大きくなるにつれて徐々に開発速度が低下していくかどうかを分ける決定的な要素となる。