C#は、Microsoftによって開発されたモダンでパワフルなプログラミング言語であり、その設計の根幹にはオブジェクト指向プログラミング(OOP: Object-Oriented Programming)の思想が深く根付いています。

大規模なシステム開発からゲーム開発(Unity)、クラウドサービスまで、C#が広く採用されている理由は、このOOPの原則に基づいた高い保守性と再利用性にあります。

オブジェクト指向をマスターするためには、その基盤となる「4つの柱」を理解することが不可欠です。

これらは、カプセル化(Encapsulation)、継承(Inheritance)、ポリモーフィズム(Polymorphism)、抽象化(Abstraction)と呼ばれます。

本記事では、これら4大原則がC#においてどのように実装され、どのようなメリットをもたらすのかを、具体的なプログラム例を交えながら詳細に解説します。

1. カプセル化(Encapsulation)

カプセル化は、オブジェクトの内部状態(データ)を外部から直接操作できないように隠蔽し、公開されたメソッドやプロパティを通じてのみアクセスを許可する仕組みです。

これにより、データの整合性を保ち、予期せぬバグの混入を防ぐことができます。

カプセル化の目的とアクセ修飾子

C#においてカプセル化を実現するための主要な手段がアクセ修飾子です。

これにより、クラスのメンバー(フィールド、メソッド、プロパティ)の公開範囲を厳密に制御します。

  • public:どこからでもアクセス可能。
  • private:同じクラス内からのみアクセス可能。
  • protected:同じクラス、または派生クラスからアクセス可能。
  • internal:同じアセンブリ(プロジェクト)内からアクセス可能。

プロパティによるデータ保護

C#では、フィールドを private に設定し、プロパティ(get/set)を使用して値を公開するのが一般的です。

これにより、値を設定する際にバリデーション(妥当性検証)を行うことが可能になります。

C#
using System;

namespace OopConcepts
{
    // 銀行口座を表すクラス
    public class BankAccount
    {
        // 外部から直接書き換えられないよう private で保持
        private decimal _balance;

        // プロパティを通じてアクセスを制御
        public decimal Balance
        {
            get { return _balance; }
            private set { _balance = value; } // 内部でのみ変更可能
        }

        // コンストラクタ
        public BankAccount(decimal initialBalance)
        {
            if (initialBalance < 0)
                throw new ArgumentException("初期残高は0以上である必要があります。");
            
            _balance = initialBalance;
        }

        // 預金メソッド:カプセル化されたデータを安全に操作
        public void Deposit(decimal amount)
        {
            if (amount <= 0)
            {
                Console.WriteLine("預金額は正の数である必要があります。");
                return;
            }
            _balance += amount;
            Console.WriteLine($"{amount}円を預金しました。現在の残高は{_balance}円です。");
        }

        // 引き出しメソッド:ロジック(残高不足チェック)をカプセル化
        public void Withdraw(decimal amount)
        {
            if (amount <= 0) return;

            if (_balance >= amount)
            {
                _balance -= amount;
                Console.WriteLine($"{amount}円を引き出しました。現在の残高は{_balance}円です。");
            }
            else
            {
                Console.WriteLine("残高不足のため引き出せません。");
            }
        }
    }

    class Program
    {
        static void Main()
        {
            BankAccount account = new BankAccount(1000);
            account.Deposit(500);
            account.Withdraw(2000); // 残高不足のケース
            account.Withdraw(300);

            // account._balance = 1000000; // コンパイルエラー:privateなので直接アクセス不可
        }
    }
}
実行結果
500円を預金しました。現在の残高は1500円です。
残高不足のため引き出せません。
300円を引き出しました。現在の残高は1200円です。

この例では、残高 _balance を直接書き換えることはできません。

不正な値(マイナスの預金など)が入力されるのを防ぐロジックがクラス内部に隠蔽されており、利用者は安全にオブジェクトを使用できます。

これがカプセル化の大きな利点です。

2. 継承(Inheritance)

継承とは、既存のクラス(親クラス / 基底クラス)の特性を新しいクラス(子クラス / 派生クラス)に引き継ぐ仕組みです。

継承を活用することで、コードの再利用性を高め、共通の動作を一箇所で管理することが可能になります。

基底クラスと派生クラス

C#では、クラス名の後ろに : を記述することで継承を表現します。

C#は単一継承を採用しており、一つのクラスが直接継承できる基底クラスは一つだけです。

継承の具体例:乗り物システム

C#
using System;

namespace OopConcepts
{
    // 基底クラス(共通のプロパティとメソッドを定義)
    public class Vehicle
    {
        public string Brand { get; set; }
        public int Speed { get; set; }

        public void Start()
        {
            Console.WriteLine($"{Brand}が始動しました。");
        }

        public void Stop()
        {
            Console.WriteLine($"{Brand}が停止しました。");
        }
    }

    // 派生クラス:Car
    public class Car : Vehicle
    {
        public int NumberOfDoors { get; set; }

        public void Honk()
        {
            Console.WriteLine($"{Brand}がクラクションを鳴らしました:プップー!");
        }
    }

    // 派生クラス:Bicycle
    public class Bicycle : Vehicle
    {
        public bool HasBasket { get; set; }

        public void RingBell()
        {
            Console.WriteLine($"{Brand}がベルを鳴らしました:チリンチリン!");
        }
    }

    class Program
    {
        static void Main()
        {
            Car myCar = new Car { Brand = "トヨタ", NumberOfDoors = 4 };
            // 基底クラスのメソッドを使用可能
            myCar.Start();
            // 派生クラス独自のメソッド
            myCar.Honk();

            Bicycle myBike = new Bicycle { Brand = "ブリヂストン", HasBasket = true };
            myBike.Start();
            myBike.RingBell();
            myBike.Stop();
        }
    }
}
実行結果
トヨタが始動しました。
トヨタがクラクションを鳴らしました:プップー!
ブリヂストンが始動しました。
ブリヂストンがベルを鳴らしました:チリンチリン!
ブリヂストンが停止しました。

継承のメリットと注意点

継承を使用すると、「is-a」関係(Car is a Vehicle)を構築できます。

共通機能(Start/Stop)を基底クラスに持たせることで、新しい乗り物を追加する際に重複コードを書く必要がなくなります。

ただし、過度な継承(深い階層構造)はコードを複雑にし、基底クラスの変更が予期せぬ範囲に影響を及ぼすリスク(脆い基底クラス問題)があるため、継承よりもコンポジション(他のクラスをフィールドとして持つこと)を検討すべき場面もあります。

3. ポリモーフィズム(Polymorphism)

ポリモーフィズム(多態性)は、同じ名前のメソッドが、呼び出し側のオブジェクトの種類によって異なる振る舞いをする能力を指します。

これにより、具体的な型を意識せずに共通のインターフェースで操作できる「柔軟なプログラム」が書けるようになります。

ポリモーフィズムには大きく分けて、静的ポリモーフィズム(オーバーロード)動的ポリモーフィズム(オーバーライド)の2種類があります。

メソッド・オーバーライド(動的ポリモーフィズム)

基底クラスで virtual キーワードを付けたメソッドを定義し、派生クラスで override キーワードを使って再定義します。

C#
using System;
using System.Collections.Generic;

namespace OopConcepts
{
    // 基底クラス
    public class Animal
    {
        public string Name { get; set; }

        // 仮想メソッド:派生クラスでの書き換えを許可
        public virtual void MakeSound()
        {
            Console.WriteLine("動物が音を出します。");
        }
    }

    // 派生クラス1
    public class Dog : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine($"{Name}が吠えました:ワンワン!");
        }
    }

    // 派生クラス2
    public class Cat : Animal
    {
        public override void MakeSound()
        {
            Console.WriteLine($"{Name}が鳴きました:ニャー!");
        }
    }

    class Program
    {
        static void Main()
        {
            // Animal型のリストに異なる派生クラスを格納
            List<Animal> animals = new List<Animal>
            {
                new Dog { Name = "ポチ" },
                new Cat { Name = "タマ" },
                new Dog { Name = "ハチ" }
            };

            // 型を意識せず、共通のメソッドを呼び出す
            foreach (var animal in animals)
            {
                animal.MakeSound();
            }
        }
    }
}
実行結果
ポチが吠えました:ワンワン!
タマが鳴きました:ニャー!
ハチが吠えました:ワンワン!

このコードの重要な点は、foreach 文の中で animal が実際には犬なのか猫なのかを判定する if 文を書いていないことです。

オブジェクト自身が自分の振る舞いを知っているため、「同じメッセージを送っても、受け手によって反応が異なる」状態が実現されています。

メソッド・オーバーロード(静的ポリモーフィズム)

同じ名前のメソッドを、引数の数や型を変えて複数定義することです。

コンパイル時にどのメソッドを呼ぶかが決定されます。

C#
public class Calculator
{
    // 2つの整数の足し算
    public int Add(int a, int b) => a + b;

    // 3つの整数の足し算
    public int Add(int a, int b, int c) => a + b + c;

    // 小数点を含む足し算
    public double Add(double a, double b) => a + b;
}

4. 抽象化(Abstraction)

抽象化とは、複雑な現実世界の仕組みから必要な要素だけを抽出し、詳細な実装を隠すことです。

利用者は「何ができるか」を知っていればよく、「どのように実現されているか」を気にする必要はありません。

C#では、抽象化を実現するために 抽象クラス(abstract class)インターフェース(interface) を使用します。

抽象クラスによる抽象化

抽象クラスはインスタンス化できず、派生クラスに対して特定のメソッドの実装を強制します。

C#
using System;

namespace OopConcepts
{
    // 抽象クラス:具体的な形状の共通概念
    public abstract class Shape
    {
        public string Color { get; set; }

        // 抽象メソッド:実装を持たず、子クラスに実装を任せる
        public abstract double GetArea();

        public void DisplayInfo()
        {
            Console.WriteLine($"色: {Color}, 面積: {GetArea()}");
        }
    }

    public class Circle : Shape
    {
        public double Radius { get; set; }
        public override double GetArea() => Math.PI * Radius * Radius;
    }

    public class Rectangle : Shape
    {
        public double Width { get; set; }
        public double Height { get; set; }
        public override double GetArea() => Width * Height;
    }
}

インターフェースによる抽象化

インターフェースは「能力」や「契約」を定義します。

クラスがどのようなプロパティやメソッドを持つべきかを規定し、多重継承に似た柔軟性を提供します。

C#
public interface IPlayable
{
    void Play();
    void Pause();
}

public class MusicPlayer : IPlayable
{
    public void Play() => Console.WriteLine("音楽を再生します。");
    public void Pause() => Console.WriteLine("音楽を一時停止します。");
}

抽象クラス vs インターフェース

抽象化を設計する際、どちらを使うべきか迷うことがありますが、以下の違いを基準に選択します。

特徴抽象クラス (abstract)インターフェース (interface)
役割「~の一種である」(is-a関係)「~ができる」(能力・契約)
コードの実装デフォルトの実装を持てる基本は定義のみ(C# 8.0以降はデフォルト実装可)
フィールド保持できる保持できない(プロパティのみ)
多重継承不可(1つのみ)可能(複数実装できる)

C# 4大原則の相乗効果

これらの4つの柱は、独立して存在するのではなく、互いに補完し合うことで真価を発揮します。

  1. カプセル化によって、クラス内部の変更が外部に悪影響を与えないよう保護されます。
  2. 継承によって、既存の信頼できるコードをベースに新機能を拡張できます。
  3. 抽象化によって、システムの設計図をシンプルに保ち、依存関係を減らすことができます。
  4. ポリモーフィズムによって、抽象化された設計図に対して、具体的な実装を動的に差し替えることが可能になります。

例えば、最新のC#開発における依存性の注入(Dependency Injection: DI)などは、まさに「抽象化」と「ポリモーフィズム」を応用した技術です。

特定のデータベース操作クラスに依存するのではなく、インターフェースに依存するように設計することで、テスト時にはモック(偽物)のオブジェクトに差し替えるといった柔軟な運用が可能になります。

また、近年のC#(C# 9.0以降)で導入されたレコード型(records)は、不変性(イミュータビリティ)を強化したカプセル化の進化形と言えます。

このように、OOPの原則は言語の進化と共に、より簡潔で安全に記述できるようにアップデートされ続けています。

まとめ

C#におけるオブジェクト指向の4大原則は、単なるプログラミングのテクニックではなく、「変化に強いソフトウェア」を作るための設計思想です。

  • カプセル化は、データを守り、バグを封じ込める。
  • 継承は、効率的な再利用と構造化を実現する。
  • ポリモーフィズムは、コードに柔軟性と拡張性をもたらす。
  • 抽象化は、複雑さを排除し、本質的な設計を浮き彫りにする。

これらの原則を意識してコードを書くことで、あなたのプログラムはより読みやすく、メンテナンスが容易で、拡張性の高いものへと進化します。

最初は難しく感じるかもしれませんが、日常的なコーディングの中で「これはどの原則に当てはまるか?」を考える癖をつけることが、熟練のC#エンジニアへの第一歩となるでしょう。