抽象と具体で磨くオブジェクト指向設計

本書は、動くだけのコードを“変化に強い設計”へ引き上げたい開発者のためのガイドです。抽象クラスとインターフェースを使い、具体クラスを差し替えやすくする手法を図解とC#サンプルで解説。小規模リファクタリングから大規模システムまで、保守性と拡張性を同時に得る実践的なヒントを示します。さらにDIコンテナやテストダブルの導入ステップも取り上げ、学習→適用→評価の循環で理解を深めます。一歩上のOOPへ。

抽象と具体を取り込むと、オブジェクト指向はどう変わるか

概念位置づけ例(C#)主なメリット
抽象 (Abstraction)“共通点”だけを宣言し、実装は持たないinterface IMovable
{
void Move();
}
abstract class Animal
{
public abstract void Speak();
}
実装を切り離して 設計を安定化/依存方向を逆転できる
具体 (Concretion)抽象を“埋める”実体。メモリ上に生成されるclass Dog : Animal
{
public override void Speak() => Console.WriteLine(“ワン");
}
目的に応じて 差し替え・追加 が容易/再利用しやすい

ポイント

  • 抽象:型定義・契約(interface / abstract class)
  • 具体:実装クラスと生成されたインスタンス(new)、あるいは構成オブジェクト

1. “型”と“インスタンス”の二段構造をさらに強化

  • これまでも「クラス=設計図」「オブジェクト=実体」と分けて考えてきました。
  • 抽象クラス/インターフェースを導入すると、「設計図の設計図」というもう一段上のレイヤが加わり、
    • 具象クラス … ロジックを含む
    • 抽象クラス/インターフェース … 役割や契約のみという 三層構造 で整理できます。
  • IDE 上でも「抽象層だけを参照(依存)し、具体層をプラグインのように差し替える」設計が促進されます。

2. 抽象を挿入すると得られる 3 つの効果

  1. 依存関係の逆転(Dependency Inversion Principle)
    • 上位モジュールが下位の実装詳細に縛られず、テストやリファクタリングが容易。
  2. 拡張に強い(Open–Closed Principle)
    • 新しい具体クラスを追加するだけで振る舞いを拡張。既存コードは“閉じたまま”。
  3. ポリモーフィズムの全面活用
    • List<IMovable> に Car と Dog を混在させ、一括で Move() を呼び出せる。アルゴリズム側は型を意識しない。

3. 具体を“遅延決定”するデザインパターン

パターン抽象と具体の分離ポイント典型シーン
Factory Method / DI コンテナ生成を委譲し、どの具体クラスを使うかを遅延決定テスト時にモックへ差し替え
Strategyアルゴリズムを差し替えAI の難易度を変更
Template Method共通処理は上位で、可変部分だけ下位クラスに委譲ファイル取込 ↔ パーサー差分

4. “抽象”だけに頼りすぎる落とし穴

症状原因処方箋
インターフェースが細切れで多すぎる“将来必要かも”と 過度な設計実装が2つ以上現れてから抽象化する “YAGNI”
継承ツリーが深い共通化の強迫観念継承より委譲、必要なら Record 構造体 等で表現
メタ抽象(IBaseServiceManagerProvider…)名称が曖昧名前は 役割+動詞 (ILogSender) にして深掘りを防止

5. まとめ ─ 抽象と具体を意識した学習ステップ

  1. まずは具体だけでアルゴリズムを完成させる
  2. 重複や 差し替えたいポイントが見えてきたら、そこを抽象化
  3. 依存逆転(上位はインターフェース参照)へリファクタリング
  4. DI コンテナやテストダブルで “差し替え体験” を行い、メリットを実感

この流れで学ぶと「抽象⇔具体」の役割分担が腑に落ち、

  • 設計の安定性(壊れにくさ)
  • 実装の柔軟性(差し替えやテストのしやすさ)をバランス良く手に入れられます。

参考コード(C#)
public interface IAttack
{
    void Execute();
}

public class SwordAttack : IAttack
{
    public void Execute() => Console.WriteLine("斬りつけた!");
}

public class Player
{
    private IAttack _attack;
    public Player(IAttack attack) => _attack = attack;
    public void Action() => _attack.Execute();
}

// 実行側
var player = new Player(new SwordAttack()); // 具体を注入
player.Action();  // 斬りつけた!

上記では Player は IAttack(抽象)にのみ依存し、SwordAttack を差し替えれば “魔法攻撃” も “遠距離攻撃” も追加コードなしで動作します。

結論

  • 抽象 = 契約具体 = 実装+インスタンス
  • 抽象を挿入すると 保守性・拡張性・テスト容易性 が飛躍的に向上
  • ただし抽象化は 必要性が生じてから–– 過剰設計は禁物

抽象と具体のレイヤを意識的に組み込むことで、オブジェクト指向は「動くだけのコード」から 変化に強い設計 へと進化します。

訪問数 1 回, 今日の訪問数 4回