Java | Java 詳細・モダン文法:設計・実務視点 – レガシーコード対応

Java Java
スポンサーリンク

レガシーコード対応を一言でいうと

レガシーコード対応は、「よく分からないけど本番で動いている古いコード」を、
壊さずに理解し、少しずつ安全にマシな状態へ近づけていく作業です。

「全部書き直したい」という気持ちをぐっとこらえて、
「どこから触るべきか」「どこは触ってはいけないか」を見極めながら、
テスト・リファクタリング・設計の改善を少しずつ積み上げていくのが現実的な戦い方になります。


レガシーコードが怖い本当の理由

「動いているけど、なぜ動いているか分からない」

レガシーコードが怖いのは、「汚いから」ではなく、
「変更したときに何が起こるか分からないから」です。

クラスが巨大で、メソッドが長くて、グローバル変数的なものがあちこちから書き換えられていて、
しかもテストがない。

こうなると、1 行直すだけでも「本番で何か壊すんじゃないか」という恐怖がつきまといます。
レガシーコード対応の本質は、この「変更の怖さ」を少しずつ減らしていくことです。

「仕様書はコードです(しかも古い)」

レガシーコードの多くは、仕様書が古かったり、そもそも存在しなかったりします。
つまり、「今の正しい仕様」は、動いているコードの中にしかありません。

だからこそ、いきなり「理想の設計」に書き換えるのは危険です。
まずは「今のコードが何をしているか」を理解し、その振る舞いをテストで固定する。
そこから初めて、安全に形を変えていけます。


最初にやるべきこと:テストで「安全ネット」を張る

いきなりリファクタリングしない

レガシーコードを前にすると、
「この巨大メソッドをきれいに分割したい」「このクラスをちゃんと分けたい」と思うはずです。

でも、テストがない状態で形だけ変えると、
「見た目はきれいになったけど、挙動が微妙に変わってバグになった」
ということが簡単に起こります。

だから、レガシーコード対応の鉄則は
「テストを書く前にリファクタリングしない」です。

振る舞いをテストで「捕まえる」

理想はユニットテストですが、最初はもっと粗くても構いません。
例えば、あるサービスクラスの「代表的な入力と出力」をテストにしてしまう。

@Test
void calculateFee_basicPattern() {
    LegacyService service = new LegacyService();
    int fee = service.calculateFee(1000, "NORMAL");
    assertEquals(100, fee); // 今の挙動を固定する
}
Java

このテストは、「仕様として正しいかどうか」以前に、
「今のコードがこう動いている」という事実を記録するものです。

こういうテストをいくつか用意しておくと、
内部の実装をいじったときに「外から見た挙動が変わっていないか」を確認できます。
これが、レガシーコードに対する「安全ネット」になります。


レガシーコードを少しずつ分解するテクニック

巨大メソッドを「意味のかたまり」で抜き出す

レガシーコードあるあるの一つが、「1 メソッド 300 行」みたいなやつです。

例えば、こんなイメージです。

void process(Order order) {
    // 入力チェック
    ...
    // 割引計算
    ...
    // 在庫引き当て
    ...
    // 請求書生成
    ...
}
Java

最初の一歩として、「意味のかたまりごとに private メソッドに抜き出す」ことから始めます。

void process(Order order) {
    validate(order);
    int discount = calculateDiscount(order);
    reserveStock(order);
    generateInvoice(order, discount);
}

private void validate(Order order) { ... }
private int calculateDiscount(Order order) { ... }
private void reserveStock(Order order) { ... }
private void generateInvoice(Order order, int discount) { ... }
Java

この段階では、ロジック自体は変えていません。
「何をしているか」がメソッド名で見えるようにしただけです。

これだけでも、
テストを書きやすくなり、
後から「割引計算だけ差し替えたい」といった変更がしやすくなります。

副作用を減らして「テストしやすい形」に寄せる

レガシーコードは、メソッドの中でフィールドを書き換えたり、
外部サービスを直接呼んだりしがちです。

テストを書きやすくするためには、
「入力と出力がはっきりしたメソッド」を増やしていくのが有効です。

例えば、こういうメソッドがあったとします。

void applyDiscount(Order order) {
    int discount = ...; // 複雑な計算
    order.setPrice(order.getPrice() - discount);
}
Java

これを、計算部分だけでも分離します。

int calculateDiscount(Order order) {
    int discount = ...; // 複雑な計算
    return discount;
}

void applyDiscount(Order order) {
    int discount = calculateDiscount(order);
    order.setPrice(order.getPrice() - discount);
}
Java

こうすると、calculateDiscount だけをユニットテストしやすくなります。
「副作用を持つメソッド」と「計算だけのメソッド」を分けるのは、
レガシーコードをテスト可能にしていくうえで、とても重要な一歩です。


「全部書き直す」は最終手段にすべき理由

書き直しは「新しいレガシー」を生むリスクが高い

レガシーコードを見ると、「こんなの捨てて全部書き直した方が早い」と思うかもしれません。
でも、実務ではこれはかなり危険な選択です。

理由はシンプルで、
「今のコードに埋まっている細かい仕様やバグ回避の知識を、全部再現できる保証がない」からです。

書き直しプロジェクトは、
「新しいきれいなコードだけど、微妙に挙動が違うシステム」を生みがちです。
そして、その差分を埋めるために、結局またレガシーな修正が積み重なっていきます。

「書き直す価値があるか」を冷静に見極める

もちろん、
テストもなく、構造も破綻していて、
ビジネス的にも大きく仕様を変えたい、という場合は、
書き直しが選択肢に入ることもあります。

そのときでも、
「一気に全部切り替える」のではなく、
機能ごとに新旧を並行稼働させて比較する、
外側の API は変えずに中身だけ差し替える、
などの戦略を取ることでリスクを下げます。

レガシーコード対応の基本スタンスは、
「まずは既存コードを活かしながら改善する。
書き直しは、本当に必要で、かつ準備が整ったときの最終手段。」
と考えておくと、判断を誤りにくくなります。


初心者がレガシーコードと向き合うときの心構え

「きたない=ダメ」ではなく「歴史の結果」として見る

レガシーコードは、たいてい「その時点ではベストだった判断」の積み重ねです。
当時の制約、納期、メンバー構成、バージョン…
いろんな事情の結果として、今の形になっています。

それを「センスがない」と切り捨てるのではなく、
「この制約の中でどう戦ってきたのか」を読み解くつもりで向き合うと、
学べることがたくさんあります。

「完璧に理解してから触る」は無理ゲー

巨大なレガシーコードを前にして、
「全部理解してからじゃないと怖くて触れない」と感じるかもしれません。

でも、現実には「全部理解する」はほぼ不可能です。
代わりに、
「今変更したい部分に関係するところだけを、テストとログを使いながら少しずつ理解する」
というスタイルに切り替える必要があります。

テストを書き、ログを仕込み、デバッガで追いかける。
そうやって「局所的な理解」を積み重ねていくうちに、
少しずつ全体像も見えてきます。


まとめ:レガシーコード対応を自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「レガシーコード対応とは、『よく分からないけど動いている古いコード』に対して、
いきなり書き直すのではなく、まずテストで今の振る舞いを捕まえ、
巨大メソッドの分割や副作用の分離など、小さなリファクタリングを積み重ねて、
“変更しても怖くない状態”に近づけていくこと。

書き直しは最後の手段であり、
基本は『壊さずに理解し、少しずつ良くする』という姿勢で向き合う。
そのために、テスト、メソッド分割、副作用の削減、境界の明確化といった技術を使って、
レガシーコードを“敵”ではなく“手懐ける相手”に変えていく。」

タイトルとURLをコピーしました