オブジェクト指向プログラミングにおけるオブジェクトは、
実装の仕方によっていくつかの内部状態管理方法があります。
この内部状態管理の方法を正しく理解することで
バグが発生しづらく、発生しても原因が特定しやすく、
また、テストしやすいプログラムを作ることができるようになります。
この記事では内部状態の不変性に関する説明をします。
また、メソッドの副作用についても簡単に触れます。
対象者
- オブジェクト指向全般・・・と言いたいところですがJava言語を使用している人
- プログラミング歴2年生くらいまで向け
- Javaが全くわからない人はまずはクラスが書けるところまでできてから
- インスタンス化がわからないとちょっと難しいかも
説明の前に
今回の説明では「じゃんけん」を例にコードを作成しています。
以下のじゃんけんの手(戦略)と審判をあらかじめ定義しています。
(じゃんけんの判定は今回の主題と関係ないので雑な実装ですみません)
public enum HandStrategy {
/** グー */
ROCK,
/** パー */
PAPER,
/** チョキ */
SCISSORS
}
public class Referee {
public static enum Result {
LEFT_WIN,
RIGHT_WIN,
DRAW
}
public static Result battle(HandStrategy left, HandStrategy right) {
if (left == right)
return Result.DRAW;
if (left == HandStrategy.ROCK)
return right == HandStrategy.PAPER ? Result.RIGHT_WIN : Result.LEFT_WIN;
else if (left == HandStrategy.PAPER)
return right == HandStrategy.SCISSORS ? Result.RIGHT_WIN : Result.LEFT_WIN;
else
return right == HandStrategy.ROCK ? Result.RIGHT_WIN : Result.LEFT_WIN;
}
}
内部状態とは
オブジェクトの内部状態とは、インスタンス化した個々のオブジェクトが持つメンバ変数(Javaではインスタンスフィールドなどともいう)の値(複数ある場合はその組み合わせ)の事を言います。
例えば以下のような場合、
public class Hand {
private HandStrategy strategy;
public void setStrategy(HandStrategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
Hand left = new Hand();
Hand right = new Hand();
left.setStrategy(HandStrategy.ROCK);
right.setStrategy(HandStrategy.PAPER);
}
}
オブジェクトleft
は内部状態(strategy
)がグー(ROCK
)、
オブジェクトright
は内部状態(strategy
)がパー(PAPER
)
となります。
今回取り上げる題材「不変性」は変数の内容が重要ではなく、
内容がいつどのようにして変わる、あるいは変わらないのかを分類しています。
不変性の分類である「Mutable」と「Immutable」、
そして少し特別な「Stateless」について説明します。
Mutable - ミュータブル
Mutable(ミュータブル)とは「可変」という意味で、
内部状態が途中で変わることがあるオブジェクトの事を指します。
定義
public class MutableHand {
private HandStrategy strategy;
public void setStrategy(HandStrategy strategy) {
this.strategy = strategy;
}
public HandStrategy getStrategy() {
return strategy;
}
}
オブジェクトは状態を持ち(=インスタンスフィールドがある)、状態を変更するメソッドがあります。
今回は意図して状態を書き換えるメソッドがありますが、
中には意図せずに書き換えができてしまうメソッドもあります。
詳しくはImmutableの章で説明します。
実行
private static void execMutable() {
final MutableHand left = new MutableHand();
final MutableHand right = new MutableHand();
left.setStrategy(HandStrategy.ROCK);
right.setStrategy(HandStrategy.PAPER);
Referee.battle(left.getStrategy(), right.getStrategy());
// 2回目は手を変えて勝負
left.setStrategy(HandStrategy.SCISSORS);
Referee.battle(left.getStrategy(), right.getStrategy());
}
オブジェクトを格納している変数はfinal
修飾していますが、
これでは不変にはならず、オブジェクトの内部状態を変えることができる事が留意点です。
2回目の勝負をする際に新たなオブジェクトは作成せず、
既存のオブジェクトの状態を書き換えて実行しています。
特徴
不変性の理解が無い状態でクラスを実装した場合、多くはこのMutableな実装になると思います。
なぜなら、実装が一番簡単で配慮する事項がほとんど無いためです。
一方で後ほど説明するImmutableな実装は配慮する事項がありかなり面倒です。
一般的には、Mutableな実装はテストの作成が難しく、バグが混入した場合に再現性が低く原因特定に時間がかかる傾向があります。(デメリット欄参照)
ですので、積極的にMutableな実装を選ぶべきでは無いです。
ただし消極的な理由、主にImmutable化が困難な場合に選択します。(なぜ困難な場合があるかは後ほど)
メリット
-
実装量は少なくなる
単にデータを格納するだけが目的の場合は最小実装で作成できます。(それが最良かどうかは議論の余地があります) -
インスタンスの再作成をせずに別の状態に変更できる
メモリ効率の点ではフィールドの変更だけで済むので追加のメモリ消費が少なく効率的と言えます。インスタンス化のコストも削減できるのでパフォーマンスにクリティカルな場面では良い場合もあります。
デメリット
-
内部状態によってメソッドの実行結果が異なる
メソッドの実行結果は内部状態に依存する可能性があるのでテストケースを状態の組み合わせの数だけ作成しないと網羅できないことがあります。特に内部状態が刻々と変わる状況ではテストを網羅できないこともあります。 -
処理の順番で結果が変わってしまう
複数のメソッドをテストする場合に、テストのために直前で実行したメソッドで内部状態が変わってしまうと次のテストに影響を及ぼしてしまいます。本番時はさらに複雑な処理になる事が想定されるので順番に影響されるとバグが混入しやすくなります。 -
状態が変わったタイミングの把握が必要になる
バグが発生した場合に再現を行おうとした場合、原因となったオブジェクトはどこでどのような内部状態に変わったか把握しないと再現が出来なかったり、「どこで内部状態が変わったか」が見つけづらくて調査に時間がかかる事がよくあります。 -
既に使用済みのオブジェクトの再利用が困難
既に使用(参照)されている場合は他の箇所で再利用することはむずかしいです。なぜなら、内部状態が意図せず書き換わってしまった場合に特定困難な不具合が発生するリスクがあるためです。
Immutable - イミュータブル
Immutable(イミュータブル)とは「不変」という意味で、
一度インスタンス化すると内部状態が変わることがないオブジェクトの事を指します。
定義
public class ImmutableHand {
private final HandStrategy strategy;
public ImmutableHand(HandStrategy strategy) {
this.strategy = strategy;
}
public HandStrategy getStrategy() {
return strategy;
}
}
オブジェクトは状態を持ちますが、状態を変更するメソッドはありません。
インスタンス化する際のコンストラクタで状態を決定します。
以後は状態を変更できないようにしなくてはなりません。
実行
private static void execImmutable() {
final ImmutableHand left = new ImmutableHand(HandStrategy.ROCK);
final ImmutableHand right = new ImmutableHand(HandStrategy.PAPER);
Referee.battle(left.getStrategy(), right.getStrategy());
// 2回目は新たにインスタンスを生成する
final ImmutableHand left2 = new ImmutableHand(HandStrategy.SCISSORS);
Referee.battle(left2.getStrategy(), right.getStrategy());
}
インスタンス化した後はそのオブジェクトは内部状態を変更することはできないので、
2回目で手を変えたい場合は、新たにインスタンス化が必要になります。
ただし、手を変えなくても良い場合は、前回使用したオブジェクトを使い回しても安全である事が保証されています。
Immutableであるなら、内容が変わる事が無いからです。
特徴
一般的にはMutableな実装よりも優れているとされているのがImmutableな実装です。
Immutableな実装ではMutableな実装にあるテスト容易性やバグ特定の問題点を一部解決できます。(メリット欄参照)
インスタンス化をする際に内部状態を決定するためのパラメータを渡す事ができますが、それ以外のタイミングでは内部状態を変更することはできません。
このことにより、既にインスタンス化しているオブジェクトに対しては何を行っても内部状態が変わらない、つまり使い回しをしてもその前後でオブジェクトは影響を受けない事が保証されます。
Immutableな実装は良いことだらけに聞こえますが、
その利点を享受するためにはとても厳しい制限事項を守る必要があります。
一つは上記で説明したインスタンス化する際に内部状態を確定しないといけないという点です。
もう一つはImmutableなオブジェクトのインスタンスフィールドも不変でなくてはならないという点です。
※実際には不変でなくても外部から不変に見えるものもImmutableとみなすという考えもありますが、少し複雑なため割愛します。
以下の実装はImmutableでしょうか?
public class NonImmutable {
private final String[] word = {"りんご", "ゴリラ", "ラッパ"};
public String[] getWord() {
return word;
}
}
一見すると内部状態を変更するメソッドはありませんね。
ですが、以下のように使用した場合、
private static void execNonImmutable() {
NonImmutable non = new NonImmutable();
non.getWord()[1] = "ゴジラ";
System.out.println(non.getWord()[1]);
}
コンソールに表示される文字列はゴリラ
でしょうか、ゴジラ
でしょうか?
正解はゴジラ
です。
このようにMutable(変更可能)なフィールドをそのままオブジェクトの外に出してしまうと、外側で変更されてしまう可能性があるため、この実装はImmutableとは言えません。
Immutableな実装にする場合は、インスタンスフィールドの内部状態も書き換えられない事に注意しなければなりません。(内部状態の内部状態の・・・と遡って全て不変である必要があります)
Javaのライブラリには変更不可能にするための機能がいくつかあります。
例えば、
Collections.unmodifiableList(list)
上記のメソッドを使用すると既存のリストから変更不可能なリストを作成する事ができます。
こういった便利機能を使用して不意に変更されないように予防するのが良いでしょう。
メリット
-
メソッドの実行順に結果が左右されない
テストで複数のメソッドをテストする場合でも順番に影響されることはありません。 -
状態の把握が容易
インスタンス化した際のパラメータのみが内部状態に影響を与えるので、状態の把握が簡単です。 -
オブジェクトの使い回しが簡単
オブジェクトは使い回してもそのオブジェクトの状態は確実に影響が無い事が保証されているので考慮する事項がありません。 -
メモリ効率が良い場合がある
使用するオブジェクトをあらかじめ作成しておいて使い回すように実装すれば、メモリ効率は良くなります。ただしこれは状態のパターンが限られていて予測可能な場合です。(予測不可能な場合は逆にデメリットにもなります)
デメリット
-
メモリ効率が悪い場合がある
インスタンス化すると内部状態を変更できないので、基本的には別の状態を持つ新しいオブジェクトを作成する必要があります。これは状態のパターンが数多くある場合不利に働きます。 -
内部状態によってメソッドの実行結果が異なる
メソッドの実行結果は内部状態に依存する可能性があるのでテストケースを状態の組み合わせの数だけ作成しないと網羅できないことがあります。ただそれでもMutableよりはテストしやすいと言えます。 -
制限事項により実装が複雑になる(または不可能)
Immutableな実装のフィールドは外部に晒す事ができないので、防御的コピーなどの複雑な実装を余儀なくされる事があります。また、インスタンス化時点では状態を特定できない場合はImmutable化は困難です。
※防御的コピーとは、参照を返すのではなく、実態の複製を作成して別オブジェクトにした後に返す手法のことです。
(これもオブジェクトの内部状態の内部状態の・・・と遡って複製する必要があります)
Immutableなフィールドは防御的コピーをする必要が無いので、インスタンスフィールドが全てImmutableならそのまま参照を返しても問題ありません。
つまりオブジェクトの大多数がImmutableであればデメリットを大幅に緩和できるということでもあります。
Stateless - ステートレス
Stateless(ステートレス)とは「状態を持たない」という意味で、
インスタンス化する/しないに関わらずオブジェクトに状態が無い事を指します。
Statelessは前提条件としてImmutableでもあります。
不変性においてはImmutableに分類されますが、追加でいくつかの特徴があるためあえて別枠としました。
定義
public abstract class StatelessHand {
private static final Rock ROCK_IMPL = new Rock();
private static final Paper PAPER_IMPL = new Paper();
private static final Scissors SCISSORS_IMPL = new Scissors();
public static StatelessHand of(HandStrategy strategy) {
switch(strategy) {
case ROCK:
return ROCK_IMPL;
case PAPER:
return PAPER_IMPL;
case SCISSORS:
return SCISSORS_IMPL;
}
throw new IllegalArgumentException();
}
public abstract HandStrategy getStrategy();
public abstract String getName();
private static class Rock extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.ROCK;
}
@Override
public String getName() {
return "グー";
}
}
private static class Paper extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.PAPER;
}
@Override
public String getName() {
return "パー";
}
}
private static class Scissors extends StatelessHand {
@Override
public HandStrategy getStrategy() {
return HandStrategy.SCISSORS;
}
@Override
public String getName() {
return "チョキ";
}
}
}
オブジェクトは状態を持ちません。(インスタンスフィールドが無い)
各実装クラス(Rock
,Paper
,Scissors
)のメソッドは固定値を返します。(=結果が変わらない)
もう1つ既にStatelessなオブジェクトが登場済みです。
それは、Referee
です。
実行
private static void execStateless() {
final StatelessHand left = StatelessHand.of(HandStrategy.ROCK);
final StatelessHand right = StatelessHand.of(HandStrategy.PAPER);
System.out.println(left.getName() + " VS " + right.getName());
Referee.battle(left.getStrategy(), right.getStrategy());
// 2回目は新たにインスタンスを取得する
final StatelessHand left2 = StatelessHand.of(HandStrategy.SCISSORS);
System.out.println(left2.getName() + " VS " + right.getName());
Referee.battle(left2.getStrategy(), right.getStrategy());
}
Statelessな実装はほとんどの場合インスタンス化不要です。
Referee
はインスタンス化せずにbattle
メソッドを実行しています。
オブジェクトの状態は結果(戻り値)に影響しません。
Statelessな実装では一見するとインスタンス化する必要が無いように思えますが、
StatelessHand
のサブクラスはインスタンス化しています。
これは、Statelessな実装でもポリモーフィズムを実現したい場合に有効な方法です。
特徴
Statelessなオブジェクトのメソッドは引数が同じなら結果も同じになります。
しかし、メソッドの内部で引数以外の(例えばstaticな)オブジェクトを使用している場合は同じ結果にならないように見えます。
これは、構文上は引数では無いですが、メソッドを実行するための「隠れ引数」として機能しているということで、
Statelessなオブジェクトの状態に結果が左右されないこととは別の問題となります。
「厳格なStateless1」(引数のみが結果に影響する)を実装する場合は引数以外は不変なオブジェクトを使用するようにしなければいけません。
StatelessHand
のサブクラスは不変オブジェクトのEnum
とString
をメソッドで使用しているため厳格なStatelessとなります。
※非常に厳密には、不変であってもクラスローダーがクラスをメモリにロードする際にstatic領域を初期化しますが、そこで書き換えを行うような実装の場合はやはり同じ結果にならないことはあります。
ただ、このパターンはよほど偏屈な実装をしないとアプリケーション実行中には結果が変わらないので厳格なStatelessに含めることとします。
例:
実行環境が変わると(WindowsからLinuxに変わるなど)違う結果が返るメソッド
System.lineSeparator()
(改行コードを返す)を使用している。
メリット
-
メモリ効率はほぼ最高
状態を持たないオブジェクトはどんなに使い回しても良いのでインスタンスはアプリケーションで唯一でも良いです。実装によってはインスタンス化すらしなくて良い場合もあります。 -
メソッドの実行順に結果が左右されない
テストで複数のメソッドをテストする場合でも順番に影響されることはありません。(注意事項があります。副作用欄参照) -
引数が同じならメソッドは常に同じ結果を返す
アプリケーションがいかなる状態でもメソッドの結果は引数次第なので、テストケースの作成が簡単な場合が多いです。(ただし「隠れ引数」に注意する必要があります)
デメリット
-
カプセル化できない
オブジェクト指向プログラミングの特徴であり利点であるカプセル化(ここではデータと処理をまとめる意味)が、Statelessな実装ではインスタンスフィールドが無いので行えません。 -
出番が少ない(ように見える)
ユーティリティ系のクラス以外で出番がほとんどないです。ただし、クラスとして定義するものはという意味です。
実はJava8以降では頻繁にStatelessなオブジェクトが活躍しています。
ラムダ
Statelessなオブジェクトは、一昔前のオブジェクト指向プログラミングではほとんど使われないかむしろアンチパターンのように言われていた時期がありました。
(状態が無いのでインスタンス化せずメソッドを何でもstaticで定義してしまうなど)
しかし、関数型プログラミングのノウハウがオブジェクト指向にもたらされた事により、Statelessが重要なポジションを得るようになりました。
もたらされたノウハウ全てについてはここでは説明しませんが、一例として ラムダ があります。
Javaにおけるラムダは実はStatelessな実装のオブジェクトが利用されています。
以下の例は引数にaとbとその2つの値に計算を行うラムダ式を渡すメソッドです。
calc(a, b, (x,y)->x+y)
このメソッドをラムダを使用せずに呼び出すとこうなります。
calc(a, b, new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y) {
return x + y;
}
});
BiFunction
というインターフェイスを実装したクラスをインスタンス化して渡しています。
ラムダとはクラスの実装とインスタンス化を極限まで簡略化した記述方法で、そのオブジェクトはStatelessな実装となります。
※ただし、厳格なStatelessでは無い場合があります。
下記のような実装の場合は引数以外の影響を受けてしまうためです。
final int offset = /*オフセット値*/;
calc(a, b, (x,y)->x+y+offset);
calc(a, b, new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y) {
return x + y + offset;
}
});
ラムダ式の場合は明らかに引数以外のオブジェクトを使用していることが判るので可読性は良いと思います。
Java8で登場したラムダとメソッド参照(詳細は割愛します)は旧来のJavaと文法上の特徴が異なり初学者にはとっつきにくい仕組みですが、
それらの登場によってStatelessに有意な価値が見出されるようになりましたので、使い道が無いとかアンチパターンだとか思わずに考察してみると良いです。
副作用
主に関数型プログラミングでよく扱われる(ような気がする)表現に「副作用」というものがあります。
Statelessなオブジェクトのメソッドは引数を受け取って戻り値として結果を返しますが、このメソッドを通過した際に引数で渡したオブジェクトの内部状態が変わってしまったり、先で説明した「隠れ引数」の内部状態が変わってしまうことを「副作用」と言います。
この副作用が発生してしまうと、Statelessのメリット「メソッドは常に同じ結果を返す」通りにできていないような動作をすることがあります。
実際には引数の内部状態が変わった状態で実行しているので、「引数が同じなら」では無くなるのですが、その事を見落としてしまい再現困難なバグを発生させてしまうリスクがあります。
副作用が全て悪という事ではありません。(少なくともオブジェクト指向プログラミングにおいては)
Immutableな実装も、自身は状態が変わりませんが引数に与えられた可変オブジェクトの状態を書き換えることはできます。
(引数で渡したオブジェクトの内部状態をセットアップするメソッドは良くある手法です)
※ここではいずれかのオブジェクトの状態が変わることを「副作用」と定義していますが、自身が変わらないものは副作用としない考えもあるようです。
(Immutableなオブジェクトのメソッド自体は常に副作用がないが、その中で呼んでいるMutableなオブジェクトのメソッドに副作用があると定義する考え方)
Immutable,Statelessな実装を行う場合は、合わせて副作用の無いメソッドにするか、副作用をコメント等で明示する必要があります。
まとめ
Mutableな実装は制約事項がほとんどなく簡単に作成することができます。
そのメリットがデメリットを上回る状況でしたら選択肢に含めても良いですが、まずはImmutableに実装できないか一考するようにした方が後々幸せになれます。
Immutableな実装は無意識では作れないほど制約事項が厳しいですが、それに見合った恩恵を得られます。
また、うまく実装できた時の達成感はプログラマだけに与えられる快感です(笑)
コードレビューでも「こいつ、やるな」と先輩方に一目置かれる存在になれる(はず)です。
最大の問題点である防御的コピーもImmutableな実装を組み合わせることで緩和できるため、Mutableを排除する理由にもなります。
Statelessな実装は注意事項が多くかえってバグが混入しやすくなる(故にアンチパターンと言われた)ように思えますが、適切に扱えれば再利用性がとても高く色褪せない実装になる可能性を秘めています。
たとえ不変なオブジェクトであっても、副作用があるメソッドを実装すると他の可変オブジェクトに影響を与えてしまう事にも注意する必要があります。
ここまで読んで頂いたあなたは、Mutable(可変)、Immutable(不変)という言葉を覚えた事によって既に意識してプログラミングができるようになっていると思います。
ご自身が今まで書いたコードを見直してみると今ならもっとうまく書けるようになっていませんか?
最後に、Java限定ですが不変性やその他の良い実装方法を詳しく知りたい方は書籍「Effective Java」をオススメします。
-
「厳格なStateless」は私が勝手に作った造語ですので他の人には通じません(汗) ↩