プログラム言語を勉強していると、getterやsetterという言葉を聞くことがあるかもしれません。これらはプログラムを動作させることだけを目的とするのであれば、使用する必要が発生しないため、使ったことがない人もいるでしょう。
プログラム初心者や、入社したばかりのエンジニアからは、「何のためにあるものなのか分からない」という声を聞くこともあるため、今回はそんなgetter/setterを使う意義や利便性について紹介してみます。
getter / setter とは
getterやsetterというのは、クラスのメンバ変数(プロパティ)へのアクセス方法を提供する手段の一つです。プログラム言語によって実装方法は異なり、getter / setter用に特殊な記述方法が用意されていたり、一般的なメンバ関数(メソッド)によって実現する必要がある言語などがあります。
それぞれの役割は以下のようになります。
- getter : 参照用インターフェース
- setter : 更新用インターフェース
ここでは、getterとsetterがそれぞれがどのような役割をするのかを、もう少し詳しく見ていきます。説明の簡略化のため、本項のサンプルコードは個人的に美しいと思うC#を利用して紹介します。
getter (C#)
getterはクラス内のメンバ変数について、外部に対して参照のインターフェースを設けます。
class hoge {
private int m_value;
public int Num_Value { get { return m_value; } }
}
... (使用例)
hoge obj = new hoge();
Debug.Log(obj.Num_Value); // 値の参照
これはクラスhogeの定義の中で、メンバ変数m_valueを定義し、プロパティNum_Valueのgetterにてm_valueの値を返却しています。
この実装はsetterがないため、外部からm_valueの値を更新される可能性がありません。Num_ValueはRead-Onlyなプロパティとなり、比較的利用価値が高い実装手法です。
setter (C#)
setterはクラス内のメンバ変数について、外部から更新を行うためのインターフェースを設けます。
class hoge {
private int m_value;
public int Num_Value { set { m_value = value; } }
}
... (使用例)
hoge obj = new hoge();
obj.Num_Value = 10; // 値の更新
これはクラスhogeの定義の中で、メンバ変数m_valueを定義し、プロパティNum_Valueのsetterによってm_valueの更新を行っています。
これはgetterがなく、外部からはプロパティNum_Valueの存在が分からないことになるので、実用性は乏しい実装です。
安全なクラス設計のために
getter / setterが参照・更新のためのインターフェースであることは確認できましたが、ここまでの説明では「結局何に使うのか」が紹介できていません。プログラムの設計について勉強している人はこの時点で気づいているかもしれませんが、これはいわゆるオブジェクト指向の重要な概念であるカプセル化の一つの手段です。
オブジェクト指向設計は、プログラムの各パーツの役割を明確にした上で、結びつき(インターフェース)を希薄にすることで、問題発生時の原因箇所の特定を容易とし、拡張性の高さを担保します。
getter / setterは「安全なクラス設計」を行うために欠かせない要素なので、しっかり確認していきましょう。
カプセル化 – publicでメンバ変数を定義しないのが大前提
getter / setterを使わなくても、メンバ変数そのものをpublic宣言してしまうことで、余計な手間をかけることなく、類似した実装を行うことができます。
// C#
class hoge {
public int m_value; // publicでメンバ変数を定義
}
上記実装では、m_valueがpublic定義されています。getter / setterという回りくどいものがなく、外部からのフルアクセスが可能となります。
これはクラス設計上とても良くない状況で、例えていうなら、クレジットカードや銀行のカードを暗証番号付きで落としているくらい危険です。新人プログラマーへの教育などでは「publicでのメンバ変数定義を禁止」にすることもある程です。クラス設計はセキュリティーの考え方と類似していて「基本は締める」です。
上記の実装をしていてm_valueの値に起因する問題が発生した時、原因調査の範囲はプログラム全体となってしまいます。小規模なプログラムの場合は大きな問題にはなりませんが、長い期間や大きな金額(コスト)がかかっているようなプログラムの場合は致命的です。
getter/setterを使うことで問題の特定が容易になる例について、少し見ていきましょう。
参照更新処理のフック – getter / setter内で処理を捕まえる
getter / setterは、その中で任意の処理を行うことが可能です。
例えば上記のようなメンバ変数の値に起因する問題が発生した場合、 setterを使用した実装をしていた場合は、以下のように問題の調査をしたり、緊急的な対応が可能となります。
class hoge {
private int m_value;
public int Num_Value {
set {
// 更新可能な値の制限
if(value < 100) m_value = value;
}
}
}
上記例では、更新可能な値の範囲についてクラス側で制限を設けた実装となっています。
もちろん判定だけではなくその他の処理も行うことも可能です。緊急で異常値を除外する場合にはもちろん有効ですし、異常値を発生する原因を特定する(デバッグでブレークポイントを張って監視する等)にも役立ちます。
setterを使うことで、値の監視をしたり更新時に任意の処理を挟み込むことができ、実装は少し回りくどいですが、これはプログラムの開発効率を全体的に高めることに役立ちます。
Read-Onlyな「独自のプロパティ」を定義可能
ちょっと役立つgetterについても紹介しておきます。以下の例では、対応するメンバ変数のいないRead-Onlyなプロパティを定義しています。
class hoge {
private int m_A;
private int m_B;
public int Num_A { get { return m_A; } }
public int Num_B { get { return m_B; } }
// 独自のプロパティを定義 (Read-Only)
public int Num_C { get { return (m_A + m_B); } }
}
m_Aとm_Bは、それぞれ対応するプロパティ(Num_AとNum_B)と共にgetter定義がされているので、ここまでに紹介した通常の実装になります。
特徴的なのはNum_Cというプロパティで、これは対応するメンバ変数がなく、ここでは例としてm_Aとm_Bの値を足し合わせた数を返却しています。このようにgetterを使うことで、クラス内部では管理していない値を、外部に対しては管理しているかのように振舞って見せることが可能です。
当然値としては管理していないプロパティ(上記例でのNum_C)に対してのsetterは、メンバ変数を更新するだけといった単純な実装はできませんが、計算処理などを行った上で各メンバ変数を更新するといった処理を実装することも可能です。
getter / setterを使用することで、内部で管理する情報と外部へのインターフェースそれぞれを意識したプログラムが可能になるでしょう。
PHPでのgetter / setterの実装
C#のgetter / setterの実装は、他の言語と比較してとてもスッキリしていて読みやすく、個人的には好感が持てます。getter / setterだけに限りませんが、同じ考え方/設計思想であってもプログラム言語によって様々な特徴や個性があります。
ここではPHPというプログラム言語のgetter / setterについて、実際の実装方法などを紹介します。PHPでもC#と同じカプセル化の機能は提供されていますが、C#と違って個別の定義が許されておらず、マジックメソッドという特定の関数に集約されている点が特徴的です。
マジックメソッドというのはPHPの機能の一つで、「ある動作がオブジェクトに対して行われた場合に、 PHP のデフォルトの動作を上書きする特別なメソッド」と定義されています。
getterの実装 (PHP)
PHPのgetterは、マジックメソッド__getを利用することで実装します。
class hoge {
private $m_value;
public function __get($prop) {
switch($prop) {
case "Num_Value":
return $this->m_value;
}
}
}
//...(使用例)
$obj = new hoge();
var_dump($obj->Num_Value);
C#と違って、一つの__getという関数にgetterが集約されていて、プロパティ名($prop)を受け取った上でプログラマが自分で振り分け処理を実装する必要があります。あまり美しくないこの実装方法ですが、派生クラスで処理を親に丸投げすることができるなど、メリットが全くない訳でもありません。
// 上記switch分のdefaultとして、存在しないプロパティの要求は親に丸投げ
default: return parent::__get($prop);
ただ、公開するプロパティが多くなってくると、どうしても__getの中が煩雑になりやすく、機能の多いクラスは可読性が低下しやすい傾向にあるでしょう。
setterの実装 (PHP)
PHPのsetterは、getterと同じくマジックメソッド__setを利用することで実装します。
class hoge {
private $m_value;
public function __set($prop, $value) {
switch($prop) {
case "Num_Value":
$this->m_value = $value;
}
}
}
// ...(使用例)
$obj = new hoge();
$obj->Num_Value = 10;
マジックメソッド__setにて、$propでプロパティ名を、$valueで値を受け取っています。プロパティ名での処理振り分け(switch)はプログラマの仕事です。
C#と同じく、メンバ変数と接続しないプロパティの定義は可能なので、Read-Onlyなgetterの実装も可能ですし、setterで処理をフックして各種処理を行うことが可能なのも同様です。
__setの中に複雑なロジックを作成すると可読性が低下しやすいため、そういった場合は処理を切り出したprivateなメンバ関数などに処理を委譲しましょう。状況によっては、__setというマジックメソッドを使わず、publicにメンバ関数を公開し、外部からはそのインターフェースでのみ更新が可能とする設計も良いでしょう。
class hoge {
private $m_value;
// setterを使わずに、通常のpublic関数で実装
public function set_Num_value($value) {
$this->m_value = $value;
}
}
更新処理をどのように扱うかによって、設計・実装は変化することになるでしょう。外部からの更新とクラス内部での更新の処理を「統一するか/切り分けるか」によっても、実装方法は異なってくるはずです。常に最適な方法は何かを考えながら実装を進めることが大事です。
堅牢で安全なクラス設計を心がけよう
今回はクラス設計の中での「カプセル化」において、重要な役割を担うgetter / setterについて、その重要性と利便性について簡単に紹介してみました。
プログラム制作(コーディング)という作業は、目的に対していくつもの解決方法があります。最適な方法というのは時と場合によっても様々で、絶対的な答えはないものです。しかし、このオブジェクト指向のような先人たちが積み上げて発展させてきた考え方には、それ相応の意味や効果があります。目先のコーディング量が増えることよりも、その後長期間にわたって不安定な爆弾を抱えることの方が、長い目で見た時に作業時間の増大になることが往々にしてあるため、それを回避するためにこういった設計技術が発展してきたと考えられます。
特に複数人で協力してコーディングをする場合などは、こういったカプセル化やインターフェースの設計には気を配らなければなりません。どこに何の処理が埋もれているのか分からず、類似した処理が複数の場所に実装されていて、バグの修正に時間がかかるような劣悪な状態のプログラムの事は、業界ではスパゲッティと呼ばれてエンジニアから忌み嫌われます。自分の制作したプログラムがスパゲッティにならないように、各クラスの役割はどういったものなのかを再確認しながら、美しくまとまった実装を心がける努力や工夫が必要です。
堅牢で安全なプログラムを制作するために、getter や setterのようなカプセル化を助けるプログラム言語の機能を積極的に利用していきましょう。