オブジェクト指向設計における状態保持の最小化と不変性のメリット

こんにちは。ゆうせいです。

プログラミングにおけるクラス設計では、そのクラスが内部にどのような情報を持つべきかという判断が重要になります。特に、計算結果や一時的な状態をフィールドとして保持するか、あるいはメソッドの戻り値として即座に返すかという選択は、プログラムの保守性や可読性に大きな影響を与えます。今回は、数当てゲームを題材とした2つのクラスを比較し、状態を持たない設計の利点について解説します。

クラスにおける状態とは何か

プログラミングにおける状態とは、クラスが内部の変数(フィールド)に記憶している情報を指します。高校の部活動に例えると、部室のホワイトボードに書かれた「現在の連絡事項」のようなものです。誰かが書き換えない限りその情報は残り続けますが、古い情報が残っていると混乱を招く原因になります。

今回比較する2つのクラス、Kazuate2とKazuateの違いは、判定結果という情報をホワイトボードに書き残しておくか、それとも聞かれたその場で口頭で答えて終わらせるかという違いに似ています。

Kazuate2クラスの構造と特徴

Kazuate2クラスは、判定結果を保持するためのmessageというフィールドを持っています。

class Kazuate2 {
	int answer;
	String message;
	Kazuate2(){
		Random random = new Random();
		answer = random.nextInt(10);
	}
	int getAnswer() {
		return answer;
	}
	String getMessage() {
		return message;
	}
	void getResult(int guess) {
		if(answer < guess) {
			message = "smaller";
		}else if(answer > guess) {
			message = "larger";
		}else {
			message = "You got it";
		}
	}
}

Kazuate2クラスの特徴は以下の通りです。

  • getResultメソッドを呼び出した際、判定結果がmessageフィールドに代入されます。
  • 判定結果を確認するためには、getMessageメソッドを別途呼び出す必要があります。
  • 一度getResultを実行すると、次に実行されるまでmessageの中身は前回の結果を保持し続けます。

Kazuateクラスの構造と特徴

一方、Kazuateクラスは判定結果をフィールドに保存せず、メソッドの戻り値として直接返します。

import java.util.Random;
class Kazuate {
	int answer;
	Kazuate(){
		Random random = new Random();
		answer = random.nextInt(10);
	}
	int getAnswer() {
		return answer;
	}
	String getResult(int guess) {
		if(answer < guess) {
			return "smaller";
		}else if(answer > guess) {
			return "larger";
		}else {
			return "You got it";
		}
	}
}

Kazuateクラスの特徴は以下の通りです。

  • getResultメソッドがString型の戻り値を持ち、判定結果を即座に返します。
  • クラス内部には正解(answer)以外の余計な情報を持ちません。
  • メソッドを呼び出した側がその場で結果を受け取るため、情報の伝達が簡潔です。

状態を持たない設計のメリットとデメリット

Kazuateクラスのように、不要な状態を持たない設計(ステートレスに近い設計)には明確な事実としての特徴があります。

メリット

  • バグの混入を防ぐことができます。Kazuate2の場合、getResultを呼ぶ前にgetMessageを呼ぶと、中身が空(null)であるために予期せぬ動作をする可能性があります。Kazuateでは、メソッドを呼べば必ず最新の結果が得られるため、呼び出し順序による間違いが起こりません。
  • プログラムの動作が予測しやすくなります。入力に対して出力が1対1で対応するため、テストやデバッグが容易になります。

デメリット

  • 計算結果を再利用したい場合、呼び出し側で変数に保存する手間が発生します。
  • 計算コストが極めて高い処理の場合、毎回計算を行うと実行速度に影響を与える可能性があります。ただし、今回の数当てゲームのような単純な比較処理では、このデメリットは無視できるほど小さいものです。

まとめと今後の学習ステップ

比較の結果、今回のケースではKazuateクラスの方が優れた設計であるといえます。判定結果という「一時的な情報」をクラスの状態として固定せず、必要なときにだけ生成して受け渡すことで、プログラムはより安全で簡潔なものになります。

今後の学習として、以下のステップを推奨します。

  1. カプセル化の概念を学び、フィールドへの直接アクセスを制限する理由を理解してください。
  2. 副作用という言葉を調べ、メソッドが外部の状態を書き換えることがどのような影響を及ぼすか学習してください。
  3. 不変オブジェクト(Immutable Object)の考え方に触れ、状態が変わらない設計の堅牢さを体験してください。

この考え方を身につけることで、大規模なシステム開発においても壊れにくいコードを書くことができるようになります。

前回の解説では、クラス内部に余計な状態(message)を持たない設計の利点を確認しました。今回はさらに一歩進んで、一度作成されたらその中身が絶対に変わらない「不変オブジェクト(Immutable Object)」の考え方を導入します。これにより、プログラムの安全性はさらに高まります。

不変オブジェクトとは何か

不変オブジェクトとは、インスタンスが生成された後、その内部状態(フィールドの値)を一切変更できないオブジェクトのことです。

高校のテスト用紙に例えるとわかりやすいでしょう。配られた瞬間に問題(正解の値)は固定されており、後から誰かが書き換えることはできません。受験者はその固定された問題に対して解答を行い、結果を受け取ります。もし問題が途中で書き換わってしまう可能性があるとしたら、公平な採点は不可能です。プログラミングにおいても、値が途中で変わらないことは「信頼性」に直結します。

Immutableを適用したKazuateクラスの設計

不変性を意識してJavaで記述する場合、フィールドにfinal修飾子を付与し、値を書き換えるメソッド(セッターなど)を排除します。

import java.util.Random;

final class ImmutableKazuate {
	
	private final int answer;
	
	ImmutableKazuate() {
		Random random = new Random();
		this.answer = random.nextInt(10);
	}
	
	int getAnswer() {
		return answer;
	}
	
	String getResult(int guess) {
		if (answer < guess) {
			return "smaller";
		} else if (answer > guess) {
			return "larger";
		} else {
			return "You got it";
		}
	}
}

この設計における変更点と事実は以下の通りです。

  • クラスとフィールドにfinal修飾子を付与しています。これにより、answerの値はコンストラクタで決まった後、二度と変更できません。
  • getResultメソッドは、内部の状態を一切書き換えません。単にanswerと比較して文字列を返すだけの「純粋な処理」になっています。

不変オブジェクトを導入するメリット

不変オブジェクトを採用することで、以下のような具体的な効果が得られます。

スレッドセーフの確保

複数の処理が同時に一つのオブジェクトにアクセスしても、値が変わることがないため、データの整合性が崩れる心配がありません。これは、複数の生徒が同時に同じ掲示板の内容を読み取っても、内容が書き換わらないため混乱が起きない状態と同じです。

参照渡しの安全化

オブジェクトを他のメソッドに引数として渡した際、そのメソッド内で勝手に値が書き換えられるリスクがゼロになります。渡した側は、戻ってきた後もオブジェクトが元の状態であることを確信して処理を続けられます。

キャッシュの活用

値が変わらないことが保証されているため、一度計算した結果を再利用しやすくなります。状態が変わる可能性のあるオブジェクトでは、常に最新の状態を確認し直す必要がありますが、不変オブジェクトではその必要がありません。

状態を持つ設計との比較まとめ

今回の不変設計と、前回のKazuate2(状態を持つ設計)を比較すると、以下の違いが明確になります。

特徴Kazuate2 (可変・状態あり)ImmutableKazuate (不変)
answerの変更誤って書き換える可能性がある物理的に書き換え不可能
判定結果の保持messageフィールドに保存される保存せず、その都度返す
呼び出し順序の制約getResultの後にgetMessageが必要制約なし
安全性低い(バグの原因になりやすい)高い(予測可能性が高い)

学習のステップと次なる目標

不変オブジェクトの考え方を理解した後は、以下の順序で学習を深めてください。

  1. JavaのStringクラスやWrapperクラス(Integerなど)が、実は不変オブジェクトとして設計されている理由を調べてください。
  2. 「副作用のない関数」という概念を学び、メソッドが外部に影響を与えないことの利点を論理的に整理してください。
  3. 大規模なリストやコレクションを扱う際に、不変性を保ちながら新しい状態を作成する方法(コピーオンライトなど)を学習してください。

不変性を味方につけることで、複雑なロジックでも迷子にならない堅牢なプログラムを構築できるようになります。

不変オブジェクトの解説に続き、今回は「副作用のない関数(純粋関数)」という考え方を、具体的なコードに当てはめて解説します。不変オブジェクトが「データの持ち方」に関するルールだとすれば、副作用のない関数は「処理の行い方」に関するルールです。

副作用とは何か

プログラミングにおける副作用とは、関数やメソッドが、その戻り値を返す以外に「外部の状態を変えてしまうこと」を指します。

高校の数学で習う関数 f(x) = x + 1 を思い出してください。この関数は、同じ x を入れれば必ず同じ結果を返し、ノートの余白を勝手に書き換えたり、教室の温度を変えたりはしません。これに対して、前述したKazuate2クラスのgetResultメソッドは、実行するとクラス内部のmessageという変数を書き換えてしまいます。これが副作用です。

副作用のない関数の適用例

副作用を排除し、純粋な計算のみを行うように設計したメソッドの例を見てみましょう。

class PureKazuate {
    
    private final int answer;

    PureKazuate(int answer) {
        this.answer = answer;
    }

    // 副作用のない関数(純粋関数)
    String getResult(int guess) {
        if (answer < guess) {
            return "smaller";
        } else if (answer > guess) {
            return "larger";
        } else {
            return "You got it";
        }
    }
}

このgetResultメソッドが「副作用のない関数」である理由は、以下の事実にあります。

  • 外部の変数(answerや他のフィールド)を一切書き換えていません。
  • 引数として受け取ったguessの値を加工して戻り値を決めるだけで、自分自身の状態を変化させません。
  • 同じanswerとguessの組み合わせであれば、何度実行しても100パーセント同じ文字列を返します。

副作用を排除するメリットとデメリット

メソッドから副作用を取り除くことで、プログラムの品質は客観的に次のように変化します。

メリット

  • 実行のタイミングに左右されなくなります。副作用がある場合、実行する順番を間違えると結果が変わってしまいますが、純粋な関数はいつでも安心して呼び出すことができます。
  • 単体テストが非常に容易になります。特定の入力を与えて期待通りの戻り値が返ってくるかを確認するだけで済み、クラス内部の状態をあらかじめ準備する手間が省けます。

デメリット

  • 画面への出力(System.out.println)やデータベースへの保存も「副作用」の一種であるため、すべての処理から副作用を消すことは不可能です。
  • 状態を更新する代わりに新しい値を生成して返す必要があるため、一時的にメモリの消費量が増える場合があります。

状態保持と副作用の関係

前回の「不変オブジェクト」と今回の「副作用のない関数」を組み合わせることで、クラス設計は非常に強固なものになります。

  1. 不変オブジェクトにすることで、外部から状態を壊されるリスクを消す。
  2. メソッドを副作用のない関数にすることで、内部から状態を壊すリスクを消す。

この2つのガードによって、開発者は「今この変数の値はどうなっているか」という不安から解放され、論理的な思考に集中できるようになります。

学習のステップと今後の展望

副作用のない関数の概念を習得した後は、以下のステップで学習を進めてください。

  1. 「参照透過性」という用語を調べ、なぜ副作用がないことが数学的な証明のしやすさにつながるのかを理解してください。
  2. JavaのStream APIなどを使い、データの集合に対して副作用のない処理を連鎖させる記述方法(宣言的プログラミング)に触れてみてください。
  3. どうしても副作用が必要な処理(ログ出力やDB更新)を、プログラムのどこに隔離すべきかという「関心の分離」について学習してください。

プログラムを「状態の変更」ではなく「データの変換」として捉えられるようになると、コードの書き方が劇的に変わります。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。