新人エンジニア研修で知っておきたいカプセル化の使い方

なぜ、カプセル化の理解が重要なのか、その理由。

この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。

前回は継承(拡張)について解説しました。今回はカプセル化について解説します。

カプセル化はデータ隠蔽とセットなのでデータ隠蔽についてもお話しします。これらもオブジェクト指向特有の概念です。

カプセル化とは、オブジェクトのデータとそれに関する処理を一つの単位(クラス)にまとめ、データを外部から直接操作されないようにする仕組みですオブジェクト指向はプログラムを部品と考えて、その部品の組み合わせで大きなプログラムを作るのです。物理的なモノの世界では車のエンジンユニットだけを交換する、デスクトップパソコンの電源ボックスだけを取り替えるということが可能です。例えば、車でもパソコンでも関連する部品は近くに配置されていますね。物理的なモノの場合は、距離の問題がありますから、関連の近いものを、近いところに並べるというのは自然です。しかし、情報の世界はそうではありませんから、気をつけないとグチャグチャになりがちです。そこでカプセル化が重要になります。

データ隠蔽とは、クラスの内部データに対する直接的なアクセスを制限し、外部からの不正な変更を防ぐ仕組みです。私たちは個々の部品、車やパソコンの中身(やそのデータ)に直接触れることができるでしょうか?もちろん、強引にやれば可能ですが(良い子の皆さんはぜひ試してみてください)保証適用外になりますね。逆に言えば、中身に直接触れさせないことによって部品と更には皆さんを守っていると言うこともできます。このイメージがデータ隠蔽です。

この記事では、カプセル化にデータ隠蔽の意味も含めて解説します。

この章のゴールは、以下のクラスに付いていたprivateとpublicの違いが説明できることです。

package chap01;

import java.util.Random;

public class Kazuate {

	private int answer;
	private String message;

	public Kazuate() {
		Random random = new Random();
		this.answer = random.nextInt(10);
	}

	public void checkAnswer(int guess) {
		if (answer > guess) {
			setMessage("もっと大きいよ");
		} else if (answer < guess) {
			setMessage("もっと小さいよ");
		} else {
			setMessage("あたり");
		}
	}

	public int getAnswer() {
		return answer;
	}

	public String getMessage() {
		return message;
	}

	private void setMessage(String message) {
		this.message = message;
	}
}

1.カプセル化の意義

カプセル化とは「関連する」情報と処理を一つのクラスにまとめることでした。

「今まで作ったクラスはどれもメンバとしてフィールドとメソッドを持っているのでカプセル化ができている」新人エンジニアであるあなたは、そう思ったかもしれません。

ポイントは“関連する”というところです。

例えば次のような一台のノートパソコンを表すクラスはどうでしょうか?

class LaptopPc{
 int id;
 String name;
 String companyName;
 String companyAddress;
 String companyTelephoneNumber;
 ・・・
}

IDと名前までは良いとしても会社名や会社住所、会社電話番号などはLaptopPcクラスが持つべきデータではないですね。以下のようにCompanyクラスを別途作りLaptopPcはCompanyオブジェクトを持つべきでしょう。

class Company {
    String name;
    String address;
    String telephoneNumber;
}

class LaptopPc {
    int id;
    String name;
    Company company;
}

クラスの設計の考え方には単一責任の原則というものがあります。

この原則は、

変更する理由が同じものは一つのクラスに集める
変更する理由が違うものは別のクラスに分ける
一つのクラスはそのクラスを利用する人に対してだけ責務を負う

という考え方です。

上記の例では会社の住所や電話番号を取得するにもLaptopPcクラスに問い合わせるということになってしまいます。また、社名が変更になった場合や会社が引っ越した場合にはLaptopPcクラスを修正することになってしまいますね。

2.データ隠蔽の意義

データ隠蔽の意義は、不用意にデータに触らせないことにあります。

ここでは、オリジナルなクラスにデータ隠蔽を施してみましょう。

例えば、カジノゲームを作成するとします。プレイヤー(Player)は最初にチップの残高(balance)を1,000持ってゲームスタートです。プレイヤーはこの残高からチップを引き出すことができますが、残高がマイナスになることはできません。

それを表現したのが以下のExample01です。

package chap11;

public class Player {

   private int balance = 1000;

   public void withdraw(int amount) {
        if ((balance - amount) < 0) {
            System.out.println("残高不足です");
        } else {
            balance -= amount;
            System.out.println("現在の残高です:" + balance);
        }
    }
}
package chap11;

public class Example01 {

    public static void main(String[] args) {
        Player p1 = new Player();
        
        p1.withdraw(200);
        p1.withdraw(1000);
    }
}

<実行結果>

現在の残高です:800
残高不足です

このようにデータ隠蔽を使ってフィールドの値を守ることができるのです。

他のプログラマ(含む将来のあなた自身)に設計者が意図しない操作をさせない、それがデータ隠蔽の効果です。

また、フィールドにアクセスするためのメソッドをアクセサメソッド【accessor method】といいます。

Javaの基本的なクラス設計には、フィールドはprivate、メソッドはアクセサメソッドとして、publicにするというものがあります。

アクセサメソッドを使ったサンプルプログラムとして、足し算電卓のクラスを作成してみます。あくまで説明のためのサンプルですので冗長でまどろこしい処理ですがご容赦ください。(本来であればnum1,num2はコンストラクタで初期化すべきでしょう。)

つまり、このようにフィールドの値を更新するときには必ずメソッドを使って更新し、フィールドには直接アクセスできなくすること」がデータ隠蔽です。公開すべきものと非公開にすべきものを明確に分け、公開したものだけを使ってプログラミングをするようにプログラマに促しているのです。

以下のCalculatorはフィールドはprivate、メソッドはpublicにしている例です。

package chap11;

public class Calculator {

    private int num1, num2;

    public int add() {
        return this.num1 + this.num2;
    }

    public int getNum1() {
        return num1;
    }

    public void setNum1(int num1) {
        this.num1 = num1;
    }

    public int getNum2() {
        return num2;
    }

    public void setNum2(int num2) {
        this.num2 = num2;
    }
}

例題

上記Calculatorクラスを以下の3つの方法で実行(例えば1+2を計算する)しなさい。

  • Calculatorクラスにメインメソッドを作成して実行
  • package chap11にCalculatorTestクラスを作成して実行
  • testパッケージを作成し、その中にCalculatorTestクラスを作成して実行

アクセサメソッドもIDEで簡単に挿入できますので試してみてください。後でWebアプリケーションを学ぶ際に活躍しますので、お楽しみに。

※ここでは、メソッドは無条件にpublicにすべきとしているように感じられるかもしれません。しかし、本当は違います。メソッドをprivateにした方が良い場合も存在します。例えば、同じクラスからしか使わないメソッドはprivateにします。メソッドをpublicにするデメリットは、そのメソッドを使う他クラスがあった場合にメソッドのインプット・アウトプットを変更しづらくなってしまうということが挙げられます(依存性の問題)。余計な公開はしないというのがデータ隠蔽の考え方です。また、アクセサメソッドはデータ隠蔽を無効にする作用があります。むやみに作るべきではありません。

※また、クラスの外から値を参照はできるが変更はされたくないフィールドには、getterのみ実装し、setterは実装しない、ということもよくあります。このお話は、またWebアプリケーションのところで出てきます。

3.アクセス修飾子

private 修飾子は、他のクラスからアクセスできない(不可視)という意味です。このように、他のクラスからインスタンス変数を隠すことを「データ隠蔽」といいました。また、修飾子とは、クラス、フィールド、メソッドの性質を指定するものをいうのでした。その中でも特に、アクセスを制御するためのものをアクセス修飾子と呼びます。

フィールドとメソッドのアクセス修飾子をアクセス範囲の狭いものから広いものの順に並べて一覧にすると下表11.1のようになります。

アクセス修飾子アクセス範囲クラス図での表記
private同じクラス内からのみ-
なし(package-private)同じパッケージのクラスからのみ~
protected同じパッケージのクラス、または別パッケージであっても、そのクラスを継承したサブクラスから#
publicどのクラスからもアクセスできる+
アクセス修飾子

下図は、クラスAの4つのフィールドに対して、どのクラスのメソッド(doSomething)からアクセス可能かを矢印(➞)を使って示しています。

分かりやすいのはどこからでもアクセス可能なpublicと自クラスからのみのprivateです。同じクラスのメソッドからは全てのフィールドにアクセス可能です。

アクセス修飾子が“なし”のフィールドは同じパッケージであれば他のクラス(下図のB)のメソッドからもアクセス可能です。パッケージプライベート【 package-private 】とも呼ばれます。

やや判断に迷いが生じるのは“protected”で、同じパッケージに加えて、サブクラスからも(サブクラス自身のフィールドとして)アクセス可能です。このことはpublicとprotectedのメンバは継承されるということをしっかり押さえればそれほど難しくはありません。

メンバのアクセス修飾子

実験1

上図クラスA~Dを作成して動作を検証しなさい。

実験結果のメモ:

なお、 protectedはWebアプリケーションを学ぶときに少しだけ出てきます。公開範囲外からのアクセスはコンパイルエラーとなります。

調べてみましょう

クラス自体にもPublicが付いたり付かなかったりします。また、クラスやメソッドにfinalが付くこともあります。このあたりのことについて詳しく知りたい方は「Javaのアクセス修飾子とfinal修飾子をやさしく解説」という記事をご覧ください

今回はカプセル化について見てきました。

カプセル化することでオブジェクトは堅牢になるのでした。

前回見た、継承とポリモーフィズム、そして今回のカプセル化の3つをあわせてオブジェクト指向の三大要素と呼ばれることがあります。

まとめができたら、アウトプットとして演習問題にチャレンジしましょう。

例題

1.データ隠蔽

以下のクラスにデータ隠蔽を施しなさい。

1度目はIDEの便利機能を使用せず、2度めは使用して作成してみること。

package chap11;

public class Q01 {
    int id;
    String name;
    boolean programmingExperience;
}
2.アクセサメソッドの効果

1で作成したクラスのアクセサメソッドに追記することにより以下の制約を課しなさい。

  • IDは整数4桁の数値 1000~9999
  • 名前は最低5文字以上
  • なお、制約を外れた値の設定に対しては何もしない(※今回はエラーメッセージも表示しない仕様)
  • コンストラクタはなし(デフォルトコンストラクタ)

作成後に別途、テストクラスを用意して動作を確かめること。

<出力例:入力制約を満たす場合>

1000
imaikatsuya

<出力例:IDに10、名前にimaiを設定した場合>

0
null
3.PINコードのクラッキング

銀行のATM等で利用されるPINコードのプロトタイプとして次のようなクラスがある。

package chap11;

class PinCode1 {

    private final int PIN;

    public PinCode1(int pin) {
        PIN = pin;
    }

    public boolean confirmCode(int nubmer) {
        return this.PIN == nubmer;
    }
}
  • このPINコードがSystem.out.println(pc.PIN)ではアクセスできないことを確かめなさい
  • このPINコードをブルートフォースアタックで解読するコードを以下に書きなさい
  • なお、PINコードは整数4桁の数値 1000~9999 とする
package chap11;

public class Q03 {

    public static void main(String[] args) {

        PinCode1 pc = new PinCode1(1234);

    //ここにコードを書く
        System.out.println(i);
    }
}

<出力結果>

1234

また、この仕様の場合、平均して何回の試行でPINが推測できるか手計算しなさい。

映画『ターミネーター2』のハッキングシーン

『ターミネーター2』では、主人公のジョン・コナーがATMをハッキングするシーンがあります。彼は、カードリーダーを使ってATMのシステムに侵入し(この部分は非現実的ですが)、4桁のPINコード(劇中では9003)を特定し、現金を引き出します。おそらく上記と同様のプログラムを使ったと推測されます。

力任せにすべての組み合わせを試すので、ブルートフォースアタックといいます。

4.PINコードのクラッキング防止

3.のインスタンスのPINコードを以下の対策で守りなさい。

・PinCode1クラスをコピーしPinCode2としこのクラスを使う

・試行回数(tryTimes)というフィールドを追加する

・試行回数が5回を超えたらシステムを強制終了する

・メインメソッドの構造には手を加えない

ヒント:System.exit(0);でシステムを強制終了させることができる。0は正常終了のコード。

<結果の例>

試行回数が5回を超えました。

以上、今回は「カプセル化と情報隠蔽で部品の完成度を高める」方法について見てきました。

次回は、「例外処理で想定外の事態に強いシステムにする」です。

例外処理を使って、途中で落ちてしまわない頑強なプログラムを作っていきましょう。