CodeGym /コース /C# SELF /アクセス修飾子とカプセル化のミス

アクセス修飾子とカプセル化のミス

C# SELF
レベル 25 , レッスン 2
使用可能

1. はじめに

アクセス修飾子って、家のフェンスやカギみたいなもんだよね:誰がどの部屋に入れるか、誰がカギ穴から覗くだけかを決めるやつ。C#ではこんな種類があるよ:

修飾子 誰が見れるか
public
みんな
private
今のクラスの中だけ
protected
今のクラスと継承したやつ
internal
プロジェクト全体(アセンブリ)
protected internal
プロジェクト全体+継承したやつ
private protected
同じプロジェクト内の継承だけ

カプセル化の目的は、クラスの実装の詳細を隠して、誰にも勝手にobj.Field = 99999;みたいなことをさせないようにすることだよ。君がOKしない限りね。

2. アクセスレベルの典型的なミス

publicで全部開けちゃう

一番ありがちなミスは、クラスのフィールドやメソッドを全部publicにしちゃうこと。「もしかしたら使うかも?他の人もどこでもアクセスできた方が便利じゃん!」ってノリ。でも、誰かがオブジェクトに変な値を入れたり、内部の詳細を勝手に使い始めると、コードを変えるのが怖くなるよ。

ミスの例:


public class BankAccount
{
    public string Owner;
    public decimal Balance;
}

これで誰でもこうできる:


var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // いきなり100万の借金!

何がダメ?
セキュリティも常識も全部無視してる。銀行が木の葉っぱやモノポリーの紙幣しか持ってなくても、「好きなだけマイナスOK」は変だよね。

クラスを「structureなのに構造がない」状態にしちゃう

もう一つよくあるのは、フィールドだけじゃなくて、内部のプロパティやメソッドまでpublicにしちゃうこと。本当は外から触っちゃダメなやつなのに。


public class Vault
{
    public string DoorPIN = "1234";
    public void OpenDoor() 
    {
        // なんか魔法
    }
}

どうなる?こうなる:誰でもPINを知って金庫のドアを開けられる。たとえ「テスト用金庫」でも、1ヶ月後にはこの穴を忘れてて、事故るかも。

正しいやり方:
クラスのインターフェースは最小限で守られてるべき。他の人が使うものだけpublic。自分だけのものはprivate。継承用ならprotected

3. カプセル化のミス

.NETでは「クラスのデータ(状態)はpublicフィールドにしちゃダメ!」ってのが鉄則。全部隠すか、最低でもプロパティでラップしよう。フィールドに直接アクセスはダサいし、バグの温床だよ。

なぜフィールドはprivateにすべきか

フィールドはオブジェクトの内部状態の核。publicにしちゃうと、いつどんな値が入るかコントロールできなくなる。変な値が入るし、フォーマット変えたら全部壊れる。

代わりにこうしよう:
フィールドはprivateにして、外には必要なプロパティやメソッドだけ出す:


public class BankAccount
{
    private decimal balance;

    public decimal Balance
    {
        get { return balance; }
        private set 
        {
            if (value < 0) 
                throw new ArgumentException("残高はマイナスにできないよ");
            balance = value;
        }
    }

    public BankAccount(decimal initialBalance)
    {
        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("0以下は入金できないよ!");
        Balance += amount;
    }
}

やっちゃダメ:public setで変えちゃいけないデータを開放

本当はgetだけでいいのに、デフォでpublic { get; set; }って書いちゃう人多い。すると誰でもこうできる:


account.Balance = 99999999; // なんでもアリ?

正しくはこう:
プロパティはクラス内だけでsetしたいなら、setに修飾子をつけよう:


public decimal Balance { get; private set; }

もしくは、値をオブジェクト生成時だけセットしたいなら(C# 14):


public decimal Balance { get; init; }

4. protectedも落とし穴になるとき

継承先にドア全開

protectedは継承先に機能を渡すのに便利だけど、データによっては継承先にも触らせたくないやつもある!例えば、子クラスにクリティカルなフィールドを直接いじらせたくない場合とか。

例:


public class SecureVault
{
    protected string secretCode = "1234";
}

これでどんな継承先でもこうできる:


public class HackerVault : SecureVault
{
    public void Hack()
    {
        secretCode = "0000"; // 簡単に秘密コード変更!
    }
}

アドバイス:
protectedは本当に継承先に必要なものだけに使おう。他はprivateにして、必要な操作はprotectedメソッドで。

5. internalのミスとアセンブリ混乱

internalは「プロジェクト内なら全部OK」って感じで便利そう。でも、プロジェクトがファイル増えたりライブラリ繋げたりすると、衝突が起きる。よく学生が、クラスを完全に隠すべきなのに、プロジェクト内でpublicにしちゃうミスをする。

例:


internal class Logger
{
    internal void Write(string msg) { /* ... */ }
}

これで誰でもプロジェクト内のどこからでもLogger.Writeを呼べる。1年後にDLLを他プロジェクトに繋げたら?publicにしたものは全部「裏側」まで見えちゃう。

ヒント:
技術的なクラスはinternalでいいけど、センシティブな部分はやっぱりprivateにしよう。

6. 実践:バンキングアプリ

バンキングアプリの例で、どんなミスが起きやすいか、どう直すかを実際に見てみよう。

ミス例1:Publicフィールド


public class BankAccount
{
    public decimal Balance;
    public string Owner;
}

問題:誰でも壊せる。

プロパティで修正


public class BankAccount
{
    private decimal _balance;
    private string _owner;

    public string Owner => _owner; // 読み取り専用

    public decimal Balance 
    { 
        get => _balance; 
        private set 
        {
            if (value < 0) throw new ArgumentException("残高はマイナスにできないよ");
            _balance = value;
        }
    }

    public BankAccount(string owner, decimal initialBalance)
    {
        if (string.IsNullOrWhiteSpace(owner))
            throw new ArgumentException("オーナー名は必須だよ");

        _owner = owner;
        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("入金額はプラスじゃないとダメ");
        Balance += amount;
    }
}

これでこうやっても:


var acc = new BankAccount("レナ", 1000m);
acc.Balance = -700m; // エラー!setはprivate。

さらにこうしても:


acc.Owner = "ハッカー"; // コンパイルエラー、setなし

— どっちも無理。コンストラクタかメソッド経由だけ。

ミス:見た目だけのプロパティ

よくあるのが:


public int Age { get; set; }

でも年齢はIncreaseAgeメソッド(例えば1月1日だけ)でしか変えちゃダメなのに、直接いじれるのはNG。

7. ビジュアルで覚えよう:ダメな例と良い例

graph TD
    A[Public Fields] -->|どこからでも| D[制御不能な状態]
    B[Private Fields + Public Methods] -->|ちゃんと定義| E[制御された状態]
    F[Public Setters] -->|誰でも変更| D
    G[Private Setters] -->|クラスだけ| E
やり方 状態は守られてる? バリデーションできる? 拡張しやすい?
Public fields
Properties (get/set public) 一部だけ できるけど危険
Private fields + setがprivateなプロパティ

8. カプセル化で気をつけるスタイルのコツ

  • 本当にクラス利用者に必要なものだけ外に出そう。
  • オブジェクトに入るデータは必ずバリデーションしよう。
  • 余計なものは出さない:外から変えちゃダメならsetはprivateに。
  • 変なロジックは必ずメソッドで。直接アクセスはやめよう。
  • 「テスト用だから」と言ってもpublicフィールドはやめよう。
  • publicで読み取りだけでも、フィールドじゃなくてプロパティを使おう。あとで拡張しやすいから。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION