ソフトウェア開発
2026/5/282 分で読める

「逆LSP(リスコフの置換原則)」の視点:生存本能が原則の破壊を命じるとき

要点

SOLIDは宗教ではなく、設計原則もまた不変の戒律ではありません。実戦を叩き上げてきたテックリードの視点に立てば、システムの命を救うために、あえてリスコフの置換原則(LSP)を破るという決断こそが、成熟したエンジニアとしての選択肢となる局面もあります。本記事では、4つの古典的なトレードオフのシナリオと、歪みを安全に閉じ込める「クリーンルーム化」の技術について深く分析します。

「逆LSP(リスコフの置換原則)」の視点:生存本能が原則の破壊を命じるとき

Clean Architectureの理論の世界において、リスコフの置換原則(LSP: Liskov Substitution Principle)は絶対的な真理です。

$S$$T$ のサブタイプであるならば、プログラムの正しさを変えることなく、型 $T$ のオブジェクトを型 $S$ のオブジェクトで置換できなければならない。」

しかし、テックリードやシニアエンジニアであるあなたなら、現実の世界が無菌室の実験室ではないことなど百も承知のはずです。午前2時、コアシステムで障害が発生しているときや、容赦ないデッドラインの圧力に晒されているとき、教科書通りの「クリーンなコード」に固執することは、時にプロジェクトを破滅へと導きます。

石油・天然ガス業界を例に挙げてみましょう。ポンプステーションへと続く生活道路の橋に、「5トン以上のトラック通行禁止」という標識が掲げられていたとします。オブジェクト指向の観点から見れば、Truck(トラック)は Vehicle(車両)を継承しています。もしこの橋を Truck が通行できないのであれば、それは明確な LSP違反(サブクラスがそのコンテキストにおいてスーパークラスを置換できていない)です。しかし、その標識は人命を救い、インフラを守るために存在しています。

ソフトウェアでも同様です。時に、意図的にLSPを違反することは、成熟したエンジニアリング上の意思決定となります。ただし、それには「Containment(隔離・局所化)」によって傷口を塞ぐ方法を知っていることが条件です。

4つの現実的シナリオ:LSPを「へし折る」ことを余儀なくされるトレードオフ

1. リファクタリング不可能なレガシーシステム(石油・ガス / ロジスティクス分野)

  • 状況: リグ(掘削装置)からの圧力データを読み込むために10年前に作られた SensorDataProcessor というインターフェースがあります。今回、新しいIoTセンサー(IoTAdvancedSensor)を統合する必要が出てきましたが、このセンサーは生のデータストリームを返さず、暗号化されたデータを返します。そのため、既存の getRawStream() メソッドを呼ぶと UnsupportedOperationException をスローせざるを得ません。

  • なぜ違反するのか?: レガシーシステム内では、何百もの依存関係がこのインターフェースに強固に結合しています。親クラスをリファクタリングすれば、システム全体にコンパイルエラーの連鎖(波及効果:Ripple Effect)を引き起こします。リグ全体のテストをやり直すために3ヶ月もの時間をかける余裕はありません。

  • 隔離策(Adapterパターン): クライアントに違反クラスを直接触らせてはいけません。それを「監禁」するのです。

Java

// レガシーインターフェース
public interface SensorDataProcessor {
    InputStream getRawStream();
    void process();
}

// 深刻なLSP違反:新しいセンサーは生ストリームをサポートしないため、例外スローを強制される
public final class IoTAdvancedSensor implements SensorDataProcessor {
    @Override
    public InputStream getRawStream() {
        // 深刻なLSP違反!
        throw new UnsupportedOperationException("IoT Sensor uses encrypted MQTT, no raw stream available.");
    }

    @Override
    public void process() {
        // 内部で暗号化ロジックを処理
    }
}

// 隔離策(Containment):Adapterを使って既存のClientを保護する
public class SensorProcessingAdapter {
    private final SensorDataProcessor processor;

    public SensorProcessingAdapter(SensorDataProcessor processor) {
        this.processor = processor;
    }

    public void SafeProcess() {
        try {
            // 特定のIoTストリームでない場合のみ、生ストリームを呼び出す
            if (!(processor instanceof IoTAdvancedSensor)) {
                InputStream is = processor.getRawStream();
                // streamの読み込み処理...
            }
            processor.process();
        } catch (UnsupportedOperationException e) {
            // エラーを完全に隔離し、メインシステムのクラッシュループを防ぐ
            Log.warn("Bypassed raw stream for IoT Sensor: " + e.getMessage());
        }
    }
}

2. パフォーマンスのクリティカルパス(性能限界への挑戦)

  • 状況: 坑井内の流体力学(Wellbore Fluid Dynamics)を計算し、オイルの流量を算出するモジュールを開発しています。システムには、リアルタイムで毎秒 $100,000$ 回のマトリックス計算(行列計算)を処理することが要求されています。

  • なぜ違反するのか?: 本来のLSPやクリーンコードに従うなら、MatrixCalculator のようなインターフェースを介してポリモーフィズムやコンポジションを適用すべきです。しかし、仮想関数テーブルの参照(Virtual Table Lookup / 動的ディスパッチ)や頻繁なオブジェクト生成は、膨大なGC(ガバベージコレクション)を誘発し、CPUスパイクの原因になります。

  • 隔離策(Private Violation, Public Clean): 外部向けのインターフェースは美しく保ちつつ、private メソッドの内部では instanceof や強引なダウンキャスト(Downcasting)を容認し、CPUキャッシュやJITコンパイラを最大限に最適化します。

Java

public interface CalculationNode {
    void execute();
}

public class HighSpeedPipeline {
    private List<CalculationNode> nodes;

    public void runCriticalPath() {
        for (CalculationNode node : nodes) {
            // 性能最適化(JITインライン化)のため、あえて抽象化の原則を破る
            if (node instanceof OptimizedGPUComputeNode) {
                // 直接キャストし、ネイティブメモリやDirectBufferへストレートにアクセス
                ((OptimizedGPUComputeNode) node).executeOnRawMemory();
            } else {
                node.execute(); // フォールバック
            }
        }
    }
}

3. サードパーティ製ライブラリという「劇薬」との付き合い方

  • 状況: 外部ベンダーの厳格なライブラリ(例:地球物理計測機器メーカーのハードウェア接続用SDK)を使用しています。そのライブラリは彼らの基底クラス(Base Class)を継承(extends)することを強制してきますが、親クラスの一部のメソッドが、こちらの現在のビジネスロジックを破壊してしまいます。

  • なぜ違反するのか?: ソースコードの所有権はこちらにありません。ベンダーに「LSPを修正してください」とプルリクエストを送るわけにもいかないのです。

  • 隔離策(Wrapper / Facade): そのライブラリ全体を包み込むラッパー(Wrapper)を実装します。LSPに違反しているそのクラスが、自システムのビジネスロジック層(ドメイン層)へ漏れ出す(Leakする)ことを絶対に許してはなりません。

Java

// サードパーティ製ライブラリ
public class VendorTelemetrySender {
    public void sendCoordinate(double x, double y) { ... }
}

// SDKの制約により、強制的に埋め込まれた違反コード
public class CustomTelemetrySender extends VendorTelemetrySender {
    @Override
    public void sendCoordinate(double x, double y) {
        if (x < 0 || y < 0) {
            // 海上リグでは負の座標を処理できないSDKの仕様に合わせるため、例外を強制
            throw new IllegalArgumentException("Offshore coordinates cannot be negative!");
        }
        super.sendCoordinate(x, y);
    }
}

// 隔離策(Containment):Wrapperがこの歪みを完全に隠蔽する
public class TelemetryService {
    private final CustomTelemetrySender vendorSender;

    public void transmit(GeoLocation location) {
        // 例外の発生を防ぐため、違反クラスにデータを渡す前に正常化(バリデーション)を行う
        double safeX = Math.max(0, location.getX());
        double safeY = Math.max(0, location.getY());
        
        vendorSender.sendCoordinate(safeX, safeY);
    }
}

4. デッドラインのための暫定ハック(リグの床が「炎上」しているとき)

  • 状況: 明日は環境省による安全監査の期日です。しかし、最新型の昇降式海洋 drillship(ジャッキアップリグ:JackUpRig)の排出指数を計算する際、システムのエミッション報告機能に深刻なバグがあることが発覚しました。

  • なぜ違反するのか?: 正しい設計を行うには、クラス階層(Class Hierarchy)を一から紐解く必要があり、3日はかかります。しかし、あなたには2時間しか残されていません。

  • 隔離策(@Deprecated + テクニカルデット・チケット): 最も不格好な if-else によるLSP違反コードを受け入れますが、後で必ず大掃除できるように「放射性マーク」をつけておきます。

Java

public class EmissionReport {
    public void generate(Rig rig) {
        // 暫定ハック: JackUpRigはここでは通常のRigとして振る舞わないため、LSPに違反
        if (rig instanceof JackUpRig) {
            // TODO: Ticket #OIL-9921 - 動的エミッション係数をサポートするためにRig階層をリファクタリングする
            System.out.println("Emergency bypass for Jack-Up Rig emission factor");
            injectHackEmission(rig);
            return;
        }
        
        rig.calculateStandardEmission();
    }

    @Deprecated(since = "2026-05", forRemoval = true)
    private void injectHackEmission(Rig rig) {
        // 監査シーズンを乗り切るための暫定的な修正ロジック
    }
}

5. ドメインが本当に複雑で矛盾しているとき(境界づけられたコンテキスト)

  • 状況: 石油開発管理において、「探鉱(Exploration)」フェーズにおける Well(油井)の概念と、「生産(Production)」フェーズにおける Well の概念はまったく異なります。探鉱フェーズには「生産量(Yield)」はなく、地質データしか存在しません。もし、これらを現実世界の名前が同じだからという理由で単一の親クラス Well にまとめてしまうと、ExplorationWellgetYield() を呼び出した時点でLSP違反が発生します。

  • なぜ違反するのか?: 初期設計の段階で、「現実世界の同名の実体は、コード上でも同一のクラスであるべきだ」という勘違いをしてしまったためです。

  • 隔離策(Bounded Context - DDD): 無理に継承を使ったりクラス構造をこねくり回したりするのをやめ、それぞれを異なるパッケージやモジュール(Exploration Context vs Production Context)へと完全に分離します。ビジネスルールが分岐してしまった以上、共通のルートを継承させるアプローチは破棄すべきです。

テックリードからの結びの言葉

ソフトウェアアーキテクチャは宗教ではなく、SOLID原則は不変の戒律ではありません。それらは、私たちが複雑さ(Complexity)という混沌を制御するための道具に過ぎないのです。

もし、あなたがコードの中に「トラック通行禁止の標識」を置く(LSPを違反する)決断を下すのであれば、以下の3つを徹底してください。

  1. 自己認識(Awareness): 自分が技術負債(Technical Debt)を借りているという事実を自覚すること。

  2. 防壁の構築(Containment): LSP違反をシステム全体へ自由に「漏出(Leak)」させないこと。Adapter、Wrapper、あるいは Private Scope を使って、その違反を確実に閉じ込めてください。

  3. 文書化(Documentation): チケットを残す、@Deprecated タグをつける、あるいは「何をしたか(What)」だけでなく「なぜしたか(Why)」を説明するコメントを残すこと。

優れたシニアデベロッパーはSOLID原則を適用する方法を知っています。しかし、卓越したテックリードは、プロジェクトを安全にゴールへ導くために、いつそれらを「破るべきか」を知っているのです。

あなたはプロジェクトを窮地から救うために、どの原則を「へし折った」ことがありますか?

コメント欄で、あなたの「消火活動」のストーリーをぜひ共有してください。

関連エントリー