なぜ、カプセル化の理解が重要なのか、その理由
この記事では、当社の新人エンジニア研修の参考に C# を解説します。
前回は「オブジェクトとメモリ領域」について学びました。メモリ領域の使い方に触れて、オブジェクト指向型パラダイムの奥深さを一層理解していただけたのではないでしょうか。
今回からは、オブジェクト指向の三大要素と呼ばれる重要概念を順に解説していきます。まずはカプセル化についてです。
1. カプセル化とは
カプセル化は英語で「encapsulation」です。文字通り「何かをカプセルにする」というような意味です。「オブジェクト指向の三大要素がなんでカプセルなの?関係あるの?」と思われた方もいらっしゃるでしょうが、関係は大ありです。なにしろ、カプセルにするのはクラスなのですから。
カプセル化とは、クラスをカプセルに見立てて、クラスが持つメンバーを外から見えるものと見えないものに分類しよう、という考え方のことです。別の言い方をすれば、クラスはブラックボックスであるべきだ、という考え方でもあります。
オブジェクト指向では機能をブレイクダウンした部品の単位がクラスであり、クラス同士の連携がアプリケーションを動作させます。ですから、当然クラスが持つ情報(データ)や処理(メソッド)にはクラスの外から利用されるものもあれば、クラス内部の処理だけに利用する、外には公開する必要のない情報や処理もあるわけです。
アプリケーションの部品が誤動作しないように、内部でしか使わないものは外からは見えないようにして強固な部品を作りましょう、というのがカプセル化の思想です。カプセル化のために、公開不要なメンバーを不可視にすることをデータ隠蔽と呼びます(実際に隠蔽するのはデータだけでなくメソッドもなのですが)。
それでは、具体的なカプセル化の方法を見ていきましょう。
クラスメンバーのスコープ ~private
とpublic
~
前章のオブジェクト指向じゃんけん、Handクラスのスタティックメンバーの定義部をご覧ください。
/**************************
* スタティックメンバー
**************************/
// 手
public const int GUU = 0;
public const int CHOKI = 1;
public const int PAA = 2;
// 有効な手の番号
private const int MIN_HAND = GUU;
private const int MAX_HAND = PAA;
// 勝ち負けを表す定数
public const int WIN = 0;
public const int LOSE = 1;
public const int DRAW = 2;
// 手の名前
private static readonly string[] HAND_NAMES = new string[] { "グー", "チョキ", "パー" };
// 勝敗表、プレーヤーの手とPCの手を要素とした二次元配列
private static readonly int[][] WINLOSS = new int[][] {
/* PCグー チョキ パー */
/* プレーヤー グー */ new int[] {DRAW, WIN, LOSE },
/* プレーヤー チョキ */ new int[] {LOSE, DRAW, WIN},
/* プレーヤー パー */ new int[] { WIN, LOSE, DRAW}
};
// 手のインスタンスの配列
public static Hand[] hands = {
new Hand(Hand.GUU),
new Hand(Hand.CHOKI),
new Hand(Hand.PAA)
};
// 手を決定する有効な番号かの真偽値を返す
public static bool isValidHand(int n) {
return (MIN_HAND <= n && n <= MAX_HAND);
}
}
定数やメソッドの前に、public
やprivate
という宣言がありますね。これがクラスメンバーの公開範囲を決める記述です。公開範囲のことを、スコープと呼び、public
やprivate
などスコープを決める宣言をアクセス修飾子と呼びます。
スコープが設定できる主な要素はクラス、フィールド、プロパティ、メソッドの4つです。クラスと他の要素では若干設定できるスコープが違いますが、全要素共通のスコープとして、まずは以下の2つを覚えましょう。
public
~ 自クラスの外に公開するメンバー どこからでも使える。private
~ 自クラス内だけで利用し、隠蔽するメンバー。
以下、各要素のスコープ設定について解説していきます。
44行目:手を表す定数:JankenGameで使うので、public
49行目:手の番号の最小値と最大値:76行目のisValidHandメソッドで使用しているだけなのでprivate
58行目:手の名前配列:コンストラクタと、ToStringメソッドで使われるだけなので、private
61行目:勝敗表:コンストラクタとjudgeメソッドで使われるだけなので、private
69行目:手のインスタンスの配列:JankenGameで使うので、public
76行目:isValidHandメソッド:JankenGameで使うので、public
publicメソッド内で利用するフィールド値でも、そのメソッド内でしか使っていないメンバーはprivate宣言するのだ、ということに注目です(上の例では、58行目の手の名前や61行目の勝敗表)。このように、クラスの外に公開する要素を必要最低限にとどめることでクラスをブラックボックス化し、独立性と再利用性を高めるのが、カプセル化のメリットです。
スコープを決めるときには、それがどんな情報(機能)で、誰が使うものなのかを考えるとわかりやすいと思います。
クラスのスコープ ~ public
、 internal
、protected
、private
~
ここで、クラスに設定するスコープについても触れておきます。public
とprivate
は既出ですが、internal
とprotected
の意味を覚えておいてください。
public
~ 自クラスの外に公開するメンバー どこからでも使える。internal
~ 同じプロジェクト内のどこからでも使えるprotected
~ 自クラスと自分を継承するクラスから使えるprivate
~ 自クラス内だけで利用し、隠蔽するメンバー。
internalは、Visual Studioで作成したソリューションの中にある、「プロジェクト」内に公開するためのアクセス修飾子です。また、protectedは、この後の章である「継承」で解説する、クラスの親子関係を作る場合に自分と自分の子クラスに限定して公開する要素に付けるアクセス修飾子です。詳しくは、次章に譲ります。
2. フィールドとプロパティ
フィールドとプロパティの違い
ここで、初心者の方がよく混乱する、フィールドとプロパティの違いと使い方について触れておきましょう。ちょうど、先ほど再掲したHand.csの冒頭に両方が宣言されています。
using System;
namespace Chap09;
// じゃんけんの手のインスタンスを生成するクラス
internal class Hand {
/**************************
* インスタンスメンバー
**************************/
// 手の名前
public string Name { get; set; }
// 勝敗表(プレーヤーの手から見たPCの手に対応する勝敗のディクショナリ)
private readonly Dictionary<int, int> winLossMap = new Dictionary<int, int>();
12行目のNameが、プロパティです。15行目のwinLossMapは、フィールドです。
項目 | プロパティ | フィールド |
---|---|---|
一般的に使われるアクセス修飾子 | public | private |
名前の最初の一文字 | 大文字 | 小文字 |
外部からのアクセス手段 | 省略形のアクセサ { get; set; } | 公開する場合は別途アクセサが必要 |
実装の実体 | メソッド | 変数 |
処理速度 | 値の入出力をメソッド経由で行うため若干遅い | 変数が格納される領域を直接参照するので早い |
追加機能 | 範囲チェックや整合性チェックなどが追加可能 | 追加機能は使えない |
イベント | 値の設定時に発生させられる | イベントは発生させられない |
用途 | クラス外から値の入出力を行うメンバー | クラス内部で処理に使うメンバー |
上記の表をご覧いただければわかるように、プロパティとは、外部に公開することが前提のフィールドをアクセサ付きで宣言する書式だと思ってくださればいいのです。つまり、上記の12行目のコードは、
// 手の名前
public string Name { get; set; }
以下のように書いたのと同じ意味になります。
// 手の名前
private string _name;
public string Name {
get { return _name; }
set { _name = value; }
}
プロパティを使うメリット
なお、プロパティを使うと、値の読み出し時と設定時の処理に、値の範囲指定や整合チェックなどの機能を追加することができます。また、プロパティに値が設定されたという、イベントを発生させることもできます。イベントについては、ラムダ式の章で詳しく説明しますが、用途と機能を考えて、どちらを使うかを決めましょう。
プロパティを使った丁寧なデータ隠蔽
例えば、クラスの外から読み出しは可能だが値の変更は許さない、という場合はこう書きます。セッターを作らないということですね。
// 手の名前
public string Name { get;}
また、同クラス内の処理からだけは変更を受け付けるプロパティは、こう書くことができます。セッターのみ、private宣言されているわけです。
// 手の名前
public string Name { get; private set; }
また、条件付きで値が変更できるようにするために、セッター内に条件分岐を書くことができます。
private int _age;
public int Age
{
get { return _age; }
private set {
if (value >= 0)
_age = value;
}
}
上でご紹介したのは、いわゆる小技の類ですが、クラスの責務を考えた実装を行う際、役に立ってくれる機能です。
以上、今回は「カプセル化と情報隠蔽で部品の完成度を高める」方法について見てきました。
次回は、「オブジェクト指向の三大要素② 継承」です。
IT企業向け新人研修おすすめ資料 無料公開中 (saycon.co.jp)