カプセル化って何?
ゲッターとセッターを覚えたところで、今回は「カプセル化」です。Humanクラスの満腹度を例に、フィールドを守りながら、外部から必要な操作ができるクラスを作ってみましょう。
カプセル状のオブジェクト?
カプセルっていうのは、その中に何かを入れて密閉してしまうような容れ物のことです。知ってますよね。
ではカプセル化というのは、どういうことなのか?
勘違いしてはいけないのは、オブジェクトを大事にカプセルの中に入れるわけではありません。オブジェクトの構造をカプセル状にしてしまうんです。
カプセル状と言うからには、中に何かを入れて密閉するわけですが、一体何をカプセルの中に大事に密閉しておくのか?それはフィールド(パラメータ)です。
全てのフィールドをprivateにすることで、まるでカプセルで覆われたように外(他のクラス)からフィールドの値を触れないように、というか見えないようにしてしまうわけです。
そして、そのカプセルには棒が刺さっています。その棒は動かすことができます。棒をカプセルの外から動かして、カプセルの中身をごちゃごちゃかき回すことができるような形状です。例えていうなら、耳かきみたいなことです。耳の中は見えないですが、耳かきを使って耳垢にアクセスすることができますよね。
一体、なんの話かというと、そのカプセルに刺さっている耳かきのような棒というのはpublicなメソッドの例えです。カプセルの外側からカプセルに突き刺さっている棒(メソッド)を使うことで、カプセルの中身(フィールド)を操作するわけです。カプセルは強固なのでその棒を使ってしか中身にアクセスできません。
気持ち悪い手書きの絵で申し訳ないのですが、カプセルに棒が刺さっていて操作できるというイメージが分かりますでしょうか。
ゲッターとセッターもこの棒の一つです。フィールドをprivateにして外部からのアクセスを遮断し、必要な操作だけをpublicなメソッドとして提供する。これがカプセル化の基本です。
ゲッター、セッターをつければいいってもんではない
もちろん、すべてのフィールドをprivateにして、ゲッターとセッターを機械的に作るだけでは、中身を十分に守れません。公開するメソッドでどんな操作を許すかが大事なんです。
例えば、以下のHumanクラスのmanpukudo(満腹度)というフィールドを見てください。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
int manpukudo;
// 省略
}上記ではインスタンスフィールドのmanpukudoにはアクセス修飾子がついていません。アクセス修飾子がついていないということは、アクセス制限はデフォルト状態なので、同一パッケージ内からなら普通にアクセスできる状態ですね。(参考:オブジェクトって何? 〔アクセス修飾子とは〕 〔ゲッターとは〕)
ということは、Humanクラスと同一パッケージ内のHumanTestクラスから、以下のようなことができてしまいます。
HumanTest.java
public class HumanTest{
public static void main(String[] args){
Human human1 = new Human("ノブオ",19770101);
human1.manpukudo = 200;
human1.manpukudo = 0;
}
}有無を言わさず満腹度を200にしたり、0にしたり。まるで胃の中に無理やり手を突っ込んでいるような状態です。まずmanpukudoをprivateにしてみましょう。さらに、単純なゲッターとセッターを作るとどうなるでしょうか。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
private int manpukudo;
// 省略
public int getManpukudo(){
return this.manpukudo;
}
public void setManpukudo(int n){
this.manpukudo = n;
}
}しかし、そのフィールドに値をセットするだけの単純なセッターを付けてしまうと、
HumanTest.java
public class HumanTest{
public static void main(String[] args){
Human human1 = new Human("ノブオ",19770101);
human1.setManpukudo(200);
human1.setManpukudo(0);
}
}セッターを使って、好き放題胃の中をいっぱいにしたり空っぽにしたりできてしまいます。これではせっかくprivateにして直接フィールドを触れなくしても、結局同じですよね。
先ほどの気持ち悪い絵のカプセルから突き出ていたのは、publicなメソッドの例えです。ゲッターやセッターもその棒の一つです。棒は刺せばいいってもんじゃありません。大事なのは、どんな機能を持った棒を刺すのか?です。余計な棒を刺したばっかりに、カプセルの中身がぐちゃぐちゃにされてしまっては、カプセルで覆った意味がなくなります。
カプセルにどんな棒を突き刺すべきか??
フィールドの性質によっては単純なセッターをつけない方がいいこともあります。満腹度は、セッターによって数値をセットするのではなく、食べれば増えて、動けば減るようにするべきです。なので、eatメソッドの他に、walkメソッドと、runメソッドを作りましょう。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
private int manpukudo;
// 省略
public int getManpukudo(){
return this.manpukudo;
}
public void eat(){
this.manpukudo += 60;
}
public void walk(){
this.manpukudo -= 10;
}
public void run(){
this.manpukudo -= 20;
}
}簡単のため、第18講のstaticフィールドの説明のために加えた食事回数を制限の部分はカットしています。
これでこのHumanオブジェクトの満腹度は、食べれば60増えて、歩けば10減って、走れば20減るようになりました。セッターはないので、満腹度が変化するのは食べる、歩く、走るのいずれかの行動をとった時に限られます。これなら、いきなり満腹度を200にしたり、0にしたりということは出来なくなりました。この方が現実に即していますよね。
ただし、やろうと思えばこんなこともできます。
HumanTest.java
public class HumanTest{
public static void main(String[] args){
Human human1 = new Human("ノブオ",19770101);
human1.run();
human1.run();
human1.run();
human1.run();
human1.run();
human1.run();
human1.run();
}
}そんなに走れるかよ。っていう状態です。これだけ走らされればmanpukudoはマイナスになっていることでしょう。けど満腹度がマイナスってどう考えてもおかしいですよね。胃が空っぽの状態でmanpukudoは0、それ以下に下がらないようにしておくべきです。同じように食べるのも限界がありますよね。このプログラムでは満腹度を0〜100の範囲で表すことにしましょう。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
private int manpukudo;
// 省略
public int getManpukudo(){
return this.manpukudo;
}
public void eat(){
this.manpukudo += 60;
if(this.manpukudo > 100){
this.manpukudo = 100;
}
}
public void walk(){
this.manpukudo -= 10;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
}
public void run(){
this.manpukudo -= 20;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
}
}初期値も0〜100にしておけば、これらの操作で満腹度が範囲を外れることはなくなりました。ただし、まだ「行動できるかどうか」の判定はありません。お腹が空っぽの時は動かず、お腹がいっぱいの時は食べないようにしてみましょう。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
private int manpukudo;
// 省略
public int getManpukudo(){
return this.manpukudo;
}
public void eat(){
if(this.manpukudo < 100){
this.manpukudo += 60;
if(this.manpukudo > 100){
this.manpukudo = 100;
}
}
}
public void walk(){
if(this.manpukudo > 0){
this.manpukudo -= 10;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
}
}
public void run(){
if(this.manpukudo > 0){
this.manpukudo -= 20;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
}
}
}if文を使って、manpukudoが100の時はeat()が呼ばれても何もしないようにしました。同じように、0の時はwalk()やrun()を呼んでも何も起こりません。
ただ、呼び出した側で行動できたかどうかを知りたい時もありますよね。そこで、行動できたらtrue、できなかったらfalseを返すようにします。ここではコンストラクタなども含めたHumanクラスを載せます。
Human.java
public class Human{
private static int count_Human = 0;
private String name;
private int birthday;
private int manpukudo;
public Human(String name, int birthday){
this.name = name;
this.birthday = birthday;
this.manpukudo = 50;
count_Human++;
}
public String getName(){
return this.name;
}
public static int getCount_Human(){
return count_Human;
}
public int getManpukudo(){
return this.manpukudo;
}
public boolean eat(){
boolean result = false;
if(this.manpukudo < 100){
this.manpukudo += 60;
if(this.manpukudo > 100){
this.manpukudo = 100;
}
result = true;
}
return result;
}
public boolean walk(){
boolean result = false;
if(this.manpukudo > 0){
this.manpukudo -= 10;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
result = true;
}
return result;
}
public boolean run(){
boolean result = false;
if(this.manpukudo > 0){
this.manpukudo -= 20;
if(this.manpukudo < 0){
this.manpukudo = 0;
}
result = true;
}
return result;
}
}それぞれのメソッドの中でローカル変数boolean resultを宣言してfalseで初期化しています。それぞれのメソッドが呼ばれた際、命令どおりに食べる、歩く、走るというのを実行した場合に限りresultにtrueを代入しています。もし、お腹いっぱいで食べられなかった場合や、お腹ぺこぺこで走れなかった場合はfalseのままです。つまり、boolean変数resultは、命令通り行動できたかどうかを示す値が入っているわけです。そのresultを戻り値としてreturnしています。
こうしておけば、メソッドを呼んだ結果、その通り実行されたかどうかが一目瞭然なので、その戻り値を使っていろんなことが可能になります。
HumanTest.java
public class HumanTest{
public static void main(String[] args){
Human human1 = new Human("ノブオ",19770101);
if(human1.run()){
System.out.println(human1.getName() + "は走った");
}else{
System.out.println(human1.getName() + "は空腹の為、走れなかった");
}
}
}↑こんな風に、メソッドを呼んだ結果によってif文で分岐させたり、
HumanTest.java
public class HumanTest{
public static void main(String[] args){
Human human1 = new Human("ノブオ",19770101);
boolean result;
do{
result = human1.eat();
}while(result);
}
}こんな風に、do-while文を使ってeatメソッドを繰り返し呼ぶこともできます。do-while文は、先にブロック内を実行してから、条件がtrueの間だけ繰り返す構文です。
満腹になった後の呼び出しでは、eat()は何もせずfalseを返し、繰り返しが終わります。最後にもう一度呼ぶのは、食べられなくなったことを確認するためなので問題ありません。
カプセルのもう一つの意味
とまあ、こういうのは一例ですが、フィールドがつじつまの合わない状態にならないようにメソッドを定義するのは、とても重要なことです。コンストラクタで設定する初期値も含めて、そのクラスのルールを守るように設計しましょう。メソッドの定義次第で、オブジェクトの使い勝手も変わってきます。
「カプセル」という言葉には、「中身を守る」だけでなく、「かっちり完成されている」というイメージもありますよね。これは言葉からの連想ですが、クラスを使いやすい一つの部品として設計する、と考えてみると分かりやすいと思います。
外部からpublicなメソッドで必要な操作ができるようにしておけば、使う側はカプセルの中身まで意識する必要がありません。もしクラス内部の仕様を変更するような時も、公開するメソッドの使い方を保てば、他のクラスへの影響を抑えやすくなります。
一つひとつのクラスでバグが起こりえない状態を作りながら、それを組み合わせることで全体を組み上げることができたら、不具合が起こりにくくメンテナンスしやすいコードになるはずです。
カプセル化はオブジェクト指向の根っこを支える重要な概念です。「privateにしたから終わり」ではなく、外部にどんな操作を許すのかまで考えて、クラスを設計していきましょう。
次回は、既存のクラスをもとに新しいクラスを作る「継承」を学びます。
質問・コメント
読み込み中この記事についての質問や、学習して気づいたことをお寄せください。
この記事のコメント
旧サイトに寄せられたコメントも当時の投稿内容のまま掲載しています。現在の記事では変更している内容や、旧環境についてのやり取りも含まれます。
コメントを読み込んでいます。