基礎から学ぶC#入門 90日コース | クラス・インスタンス・設計 - Day 36:カプセル化

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 36 のゴールと全体像

Day 36 のテーマは カプセル化 です。 いよいよ「オブジェクト指向らしさ」のど真ん中に入っていきます。

学ぶキーワードは次の 3 つです。

  • 外部から直接変更させない
  • private set
  • データの整合性

ざっくり言うと、

「クラスの中の大事なデータを、勝手に壊されないように守る」

ための考え方とテクニックを学ぶ回だと思って読んでいただけると、イメージしやすいと思います。

カプセル化とは何か

「中身を隠して、外には窓口だけを見せる」

カプセル化は、オブジェクト指向の基本的な考え方のひとつで、

「クラスの内部の仕組みを隠して、外からは必要な窓口だけを見せる」

というものです。

イメージとしては、

  • 中身が見えない「カプセル」
  • 外からはボタンやスイッチだけが見える
  • ボタンを押すと、中でいい感じに処理してくれる

という感じです。

コードの世界では、

  • フィールドや内部メソッドは private で隠す
  • 外から使ってほしいメソッドやプロパティは public で公開する

という形で実現します。

外部から直接変更させない、という発想

「何でも書き換えられる状態」は危険

例えば、銀行口座を表すクラスを考えてみます。

class BankAccount
{
    public string OwnerName;
    public int Balance; // 残高
}
C#

これをそのまま使うと、外からこう書けてしまいます。

BankAccount account = new BankAccount();
account.OwnerName = "太郎";
account.Balance = 1000;

// 外部コードが勝手に残高を書き換えられてしまう
account.Balance = -999999; // マイナス残高、ありえない値
C#

銀行口座の残高が、 どこからでも自由に書き換えられる状態は、 さすがに危険ですよね。

ここで登場するのが 「外部から直接変更させない」 という発想です。

フィールドを隠し、メソッドで操作させる

残高は private にして、操作はメソッド経由にする

先ほどの BankAccount を、カプセル化の考え方で書き直してみます。

class BankAccount
{
    // 残高は外から直接触らせないので private
    private int balance;

    // 口座名義は外から見えてもよいとする
    public string OwnerName { get; set; }

    // 残高を読み取るための public メソッド
    public int GetBalance()
    {
        return balance;
    }

    // 入金メソッド
    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("入金額は正の値でなければなりません。");
            return;
        }

        balance += amount;
    }

    // 出金メソッド
    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("出金額は正の値でなければなりません。");
            return;
        }

        if (amount > balance)
        {
            Console.WriteLine("残高不足です。");
            return;
        }

        balance -= amount;
    }
}
C#

使う側のコードは、

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount();
        account.OwnerName = "太郎";

        account.Deposit(1000);   // 入金
        account.Withdraw(300);   // 出金

        Console.WriteLine($"現在の残高: {account.GetBalance()}");

        // account.balance = -999999; // コンパイルエラー(private のため)

        Console.ReadLine();
    }
}
C#

という形になります。

ここでのポイントは、

  • 残高 balanceprivate なので、外から直接書き換えられない
  • 入金・出金は、必ず Deposit / Withdraw メソッドを通して行われる
  • メソッドの中で「正の値か」「残高不足でないか」をチェックしている

というところです。

重要ポイント:

  • 「外部から直接変更させない」ことで、 クラスの中のデータを安全に保てます。
  • これがカプセル化の具体的な効果のひとつです。

private set を使ったカプセル化

「読み取りは公開、書き込みはクラスの中だけ」

Day 32 で学んだプロパティに、 カプセル化の考え方を組み合わせると、とても便利な形になります。

それが private set です。

private set は、

「プロパティの値の読み取りは外からできるけれど、 書き込みはクラスの中からしかできない」

という意味になります。

先ほどの BankAccount を、プロパティ+private set で書き直してみます。

class BankAccount
{
    // 残高は読み取りだけ公開し、書き込みはクラス内だけに制限する
    public int Balance { get; private set; }

    public string OwnerName { get; set; }

    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("入金額は正の値でなければなりません。");
            return;
        }

        Balance += amount; // クラスの中なので set にアクセス可能
    }

    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("出金額は正の値でなければなりません。");
            return;
        }

        if (amount > Balance)
        {
            Console.WriteLine("残高不足です。");
            return;
        }

        Balance -= amount; // クラスの中なので set にアクセス可能
    }
}
C#

使う側のコードは、

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount();
        account.OwnerName = "太郎";

        account.Deposit(1000);
        account.Withdraw(300);

        // Balance は読み取り可能
        Console.WriteLine($"現在の残高: {account.Balance}");

        // account.Balance = -999999; // コンパイルエラー(set が private のため)

        Console.ReadLine();
    }
}
C#

重要ポイント:

  • public int Balance { get; private set; } という形で、 「読み取りは公開、書き込みはクラス内限定」 を簡潔に表現できます。
  • これにより、 「外から勝手に値を変えられないけれど、状態は確認できる」 という、カプセル化らしい設計ができます。

データの整合性を守る、という視点

整合性とは「矛盾のない状態を保つこと」

データの整合性とは、

「そのクラスの中のデータが、常に矛盾のない状態になっていること」

です。

例えば、

  • 残高がマイナスになっていない
  • 年齢が 0〜120 の範囲に収まっている
  • 点数が 0〜100 の範囲に収まっている

といった「ルール」を守れている状態が、 整合性が保たれている状態です。

カプセル化は、この整合性を守るための強力な武器になります。

Student クラスで整合性を守る例

class Student
{
    // 点数は読み取りだけ公開、書き込みはクラス内だけ
    public int Score { get; private set; }

    public string Name { get; set; }

    // コンストラクタで初期値を設定
    public Student(string name, int score)
    {
        Name = name;
        SetScore(score);
    }

    // 点数を設定するための内部メソッド
    private void SetScore(int score)
    {
        if (score < 0)
        {
            Score = 0;
        }
        else if (score > 100)
        {
            Score = 100;
        }
        else
        {
            Score = score;
        }
    }

    // テストの再採点などで点数を更新するメソッド
    public void UpdateScore(int newScore)
    {
        SetScore(newScore); // 整合性チェックを必ず通す
    }

    public void PrintInfo()
    {
        Console.WriteLine($"名前: {Name}, 点数: {Score}");
    }
}
C#

使う側のコードは、

class Program
{
    static void Main(string[] args)
    {
        Student s = new Student("花子", 120); // 120 → 100 に補正される
        s.PrintInfo();

        s.UpdateScore(-10); // -10 → 0 に補正される
        s.PrintInfo();

        // s.Score = 999; // コンパイルエラー(set が private のため)

        Console.ReadLine();
    }
}
C#

ここでは、

  • Score の書き込みは、必ず SetScore メソッドを通る
  • SetScore の中で、0〜100 の範囲に補正している
  • 外からは Score を直接書き換えられない

という構造になっています。

重要ポイント:

  • カプセル化によって「データの変更経路」を限定することで、 整合性チェックを必ず通すようにできます。
  • これが、カプセル化の実務的な価値のひとつです。

Day 36 用の練習テンプレート

最後に、private set とカプセル化の練習に使えるテンプレートを紹介します。

読書記録クラスの例

using System;

class BookRecord
{
    public string Title { get; private set; }
    public string Author { get; private set; }

    // 読了フラグは読み取りだけ公開、変更はクラス内だけ
    public bool IsFinished { get; private set; }

    public BookRecord(string title, string author)
    {
        Title = title;
        Author = author;
        IsFinished = false;
    }

    // 読了にするメソッド
    public void Finish()
    {
        if (!IsFinished)
        {
            IsFinished = true;
            Console.WriteLine($"「{Title}」を読み終えました。");
        }
        else
        {
            Console.WriteLine($"「{Title}」はすでに読了済みです。");
        }
    }

    public void PrintStatus()
    {
        string status = IsFinished ? "読了" : "未読";
        Console.WriteLine($"タイトル: {Title}, 著者: {Author}, 状態: {status}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        BookRecord book = new BookRecord("C#入門", "山田太郎");

        book.PrintStatus();
        book.Finish();
        book.PrintStatus();

        // book.IsFinished = false; // コンパイルエラー(set が private のため)

        Console.ReadLine();
    }
}
C#

このテンプレートをベースにして、

  • Order クラスで「合計金額は private set にして、計算メソッドだけで更新する」
  • User クラスで「登録日やユーザーIDは外から変更できないようにする」
  • TaskItem クラスで「完了フラグは Complete() メソッドだけで変更する」

など、いろいろな「カプセル化されたクラス」を作ってみると、 private set とデータ整合性の感覚がぐっと身近になります。

Day 36 のまとめ

Day 36 では、

  • カプセル化 = クラスの内部を隠し、外からは窓口だけを見せる考え方
  • 外部から直接変更させない = フィールドやプロパティの書き込みを制限する発想
  • private set = 「読み取りは公開、書き込みはクラス内限定」を簡潔に表現するテクニック
  • データの整合性 = クラスの中のデータが常に矛盾のない状態であること

という流れで、 「オブジェクトの中身を守る」という視点を学びました。

オブジェクト指向の世界では、

「何を隠して、どこからどう触らせるか」

をきちんと設計することが、 安全で壊れにくいコードにつながっていきます。

ぜひ、

  • 自分でクラスを 1 つ作って、「外から直接変えられたくないデータ」に private set を付けてみる
  • データの整合性を守るために、「変更経路」をメソッドに一本化してみる
  • 「このクラスのデータが壊れるとしたら、どこから壊れるか?」を想像して、カプセル化で防いでみる

といった練習を通して、 カプセル化を 「ただのキーワードの組み合わせ」ではなく、 クラスを守るための設計の武器として、少しずつ自分のものにしていってください。


Day 36 ミニ課題のゴール

このミニ課題では、銀行口座クラスを題材にして、カプセル化の考え方を実際のコードに落とし込んでいきます。

作る機能はシンプルです。

  • Deposit():入金
  • Withdraw():出金
  • Balance:残高の参照

そして、学びたいポイントは次の 3 つです。

  • 外部から直接変更させない
  • private set
  • データの整合性

「銀行口座」という、現実にありそうなものをイメージしながら、 オブジェクト指向らしい設計を体験していきます。

ステップ 1:銀行口座クラスのイメージを言葉にする

まずは、コードを書く前に「銀行口座とは何か」を言葉で整理してみます。

銀行口座のイメージ:

  • データ
    • 口座名義(OwnerName)
    • 残高(Balance)
  • 処理
    • 入金する(Deposit)
    • 出金する(Withdraw)
    • 残高を確認する(Balance を参照)

ここで大事なのは、

「残高は勝手に書き換えられてはいけない」

というルールです。

このルールを守るために、 外部から直接変更させない・private set・データの整合性 というキーワードが効いてきます。

ステップ 2:まずは「危ない」設計を見てみる

あえて、よくない例から見てみます。

class BankAccount
{
    // 口座名義
    public string OwnerName;

    // 残高(危険な設計)
    public int Balance;
}
C#

このクラスを使うと、外からこう書けてしまいます。

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount();
        account.OwnerName = "太郎";
        account.Balance = 1000;

        // 外部コードが勝手に残高を書き換えられる
        account.Balance = -999999; // マイナス残高、ありえない値

        Console.ReadLine();
    }
}
C#

この状態だと、

  • 残高がマイナスになってしまう
  • どこからでも好き勝手に書き換えられる

という、データの整合性がボロボロな状態になります。

ここから、カプセル化を使って「安全な銀行口座」にしていきます。

ステップ 3:外部から直接変更させない設計にする

フィールドを隠し、メソッドで操作させる

まずは、残高を private にして、 外から直接触れないようにします。

class BankAccount
{
    // 口座名義は外から見えてもよいとする
    public string OwnerName { get; set; }

    // 残高は外から直接触らせないので private フィールドに
    private int balance;

    // 残高を読み取るためのメソッド
    public int GetBalance()
    {
        return balance;
    }

    // 入金メソッド
    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("入金額は正の値でなければなりません。");
            return;
        }

        balance += amount;
    }

    // 出金メソッド
    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("出金額は正の値でなければなりません。");
            return;
        }

        if (amount > balance)
        {
            Console.WriteLine("残高不足です。");
            return;
        }

        balance -= amount;
    }
}
C#

使う側のコードはこうなります。

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount();
        account.OwnerName = "太郎";

        account.Deposit(1000);   // 入金
        account.Withdraw(300);   // 出金

        Console.WriteLine($"現在の残高: {account.GetBalance()}");

        // account.balance = -999999; // コンパイルエラー(private のため)

        Console.ReadLine();
    }
}
C#

ここで、

  • 残高は balance フィールドに隠されている
  • 入金・出金は必ず Deposit / Withdraw を通る
  • メソッドの中で「正の値か」「残高不足でないか」をチェックしている

という形になり、 外部から直接変更させない状態が作れています。

ステップ 4:private set を使って、もっとスマートに

Balance をプロパティにして、読み取りだけ公開する

次に、残高をプロパティにして、 private set を使った形にしてみます。

class BankAccount
{
    // 口座名義は普通の自動実装プロパティ
    public string OwnerName { get; set; }

    // 残高は読み取りだけ公開し、書き込みはクラス内だけに制限する
    public int Balance { get; private set; }

    // 入金メソッド
    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("入金額は正の値でなければなりません。");
            return;
        }

        Balance += amount; // クラスの中なので set にアクセス可能
    }

    // 出金メソッド
    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("出金額は正の値でなければなりません。");
            return;
        }

        if (amount > Balance)
        {
            Console.WriteLine("残高不足です。");
            return;
        }

        Balance -= amount; // クラスの中なので set にアクセス可能
    }
}
C#

使う側のコードはこうなります。

class Program
{
    static void Main(string[] args)
    {
        BankAccount account = new BankAccount();
        account.OwnerName = "太郎";

        account.Deposit(1000);
        account.Withdraw(300);

        // Balance は読み取り可能
        Console.WriteLine($"現在の残高: {account.Balance}");

        // account.Balance = -999999; // コンパイルエラー(set が private のため)

        Console.ReadLine();
    }
}
C#

ここが重要ポイントです。

  • public int Balance { get; private set; } という形で、 「残高は外から参照できるけれど、変更はクラスの中だけ」 を簡潔に表現できます。
  • 外部コードは Balance を「見ることはできる」が、「直接書き換えることはできない」状態になります。

ステップ 5:データの整合性を意識した設計にする

「残高が常に正しい状態であること」を守る

銀行口座の残高に関して、 守りたい整合性はこんな感じです。

  • 残高はマイナスにならない
  • 入金額・出金額は正の値である
  • 残高不足の出金はできない

これらを、Deposit / Withdraw の中でチェックすることで、 Balance が常に矛盾のない状態になるように守ることができます。

もう一度、整合性を意識した完成形をまとめてみます。

using System;

class BankAccount
{
    // 口座名義
    public string OwnerName { get; set; }

    // 残高:読み取りは公開、書き込みはクラス内だけ
    public int Balance { get; private set; }

    // コンストラクタで初期残高を設定(任意)
    public BankAccount(string ownerName, int initialBalance = 0)
    {
        OwnerName = ownerName;

        if (initialBalance < 0)
        {
            Console.WriteLine("初期残高がマイナスのため、0 に補正します。");
            Balance = 0;
        }
        else
        {
            Balance = initialBalance;
        }
    }

    // 入金
    public void Deposit(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("入金額は正の値でなければなりません。");
            return;
        }

        Balance += amount;
        Console.WriteLine($"{amount} 円入金しました。現在の残高: {Balance} 円");
    }

    // 出金
    public void Withdraw(int amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("出金額は正の値でなければなりません。");
            return;
        }

        if (amount > Balance)
        {
            Console.WriteLine($"残高不足です。現在の残高: {Balance} 円、出金額: {amount} 円");
            return;
        }

        Balance -= amount;
        Console.WriteLine($"{amount} 円出金しました。現在の残高: {Balance} 円");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 口座を作成
        BankAccount account = new BankAccount("太郎", 1000);

        // 状態確認
        Console.WriteLine($"口座名義: {account.OwnerName}, 初期残高: {account.Balance} 円");

        // 入金と出金
        account.Deposit(500);
        account.Withdraw(300);
        account.Withdraw(2000); // 残高不足の例

        // 外から残高を直接書き換えることはできない
        // account.Balance = -999999; // コンパイルエラー

        Console.ReadLine();
    }
}
C#

このコードでは、

  • 残高は Balance プロパティで管理し、private set で書き込みを制限
  • 入金・出金は必ず Deposit / Withdraw を通る
  • その中で「正の値か」「残高不足でないか」をチェック
  • 初期残高もコンストラクタでチェックして補正

という形で、 データの整合性を守る銀行口座クラスになっています。

ミニ課題のまとめ

このミニ課題では、

  • 銀行口座という現実の題材を使って
  • 「外部から直接変更させない」設計を考え
  • private set を使って「読み取りだけ公開、書き込みはクラス内限定」を実現し
  • 入金・出金のメソッドの中で「データの整合性」を守る

という流れで、 カプセル化の考え方を具体的なコードに落とし込みました。

同じパターンは、銀行口座以外にもたくさん応用できます。

  • ポイント残高
  • 在庫数
  • 合計金額
  • 完了フラグ

など、「勝手に書き換えられると困るデータ」には、 ぜひ private set とカプセル化の発想を使ってみてください。

「このクラスの大事なデータを、どうやって守るか?」 という視点でコードを眺めると、 オブジェクト指向の設計がぐっと面白くなっていきます。

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