謎言語使いの徒然

適当に気になった技術や言語を流すブログ。

オブジェクトの状態は作らない

ここで言う状態とは、クラス変数と振る舞いの状態を意味します。
例えば、クラス内の特定のフラグが true となったらメソッドの挙動が変わるなどです。

状態とは?

Java で例えれば、Closable インターフェース実装クラスなんかがそれに当たります。
リソースの解放と言う意味では仕方ないのでしょうが、close() を実行したあとでメソッドを呼び出すと例外を吐き出すようになります。

リソースを解放した後にアクセスできないのは当然の話ですし、close 後は使用不可(例外)を吐き出すだけまだマシと言うものでしょう。
一番怖いのは「動作が変わる」と言うパターンです。

例えばクラス自体が状態遷移図のようなものを持っている場合です。

状態遷移図(Wikipedia)

具体的にはこんな感じのクラス設計だ。

  • クラス動作に必要な値を、コンストラクタで初期化せず、setter 経由でしか設定できない。
  • メソッド呼び出しに順序が指定されている。
  • クラス変数の状態によって、getter 以外の public メソッドの挙動が変わってしまう。

何故状態が悪?

こうしたクラス設計とは、つまりクラスの内部的な動作を知らないと、意図した結果が得られない事を意味する。

オブジェクト指向の成り立ちが、もともと シミュレーション用言語 Simula なので、状態の変化と言うのはあって当然だったと思われる。
また、現実的にも自販機などのようにお金を投入された状態(ドリンク選択待機状態)など、状態を持つものは多い。

さらに、オブジェクト指向の本質、抽象化による現実の模倣(車とかがよく例に出されるが、それも状態だらけだ)としても、状態を是としている。
だが、いくつかの理由で、これはバグを産みやすくなる。

  1. (先に挙げたように)クラスの内部実装を理解していないと、正しく使えない。
    つまり、「この動作はこの機能の実装にも必要なんだけど…」と思った時に、そのクラスのコードを全て理解しないと使えないという事だ。
    状態がなく、クラス/メソッド名が確かなら、そもそもインターフェースだけで使い方がなんとなくわかる筈なのにも関わらずだ。
  2. 状態を持ったオブジェクトはテストしづらい。
    例えば、クラス内の状態を2変数で持ったとしましょう。すると、「テストすべき状態数=1変数で操作される状態数 x 1変数で操作される状態数」と倍数的に増えてしまう。
    まさかテストせずにリリースする訳にも行かないでしょう?
    テストコードを書かないとしても、結合テストでは、ここの分岐を動かすための操作、事前条件を整えるのが難しくなる。
    そうしたものは、結合レベルでは見逃す事も多い。
  3. 安心して他のメソッドに渡せない。
    オブジェクトに状態があり、それが書き換え可能だと、メソッド引数に入れて渡した時、その先で書き換えられてしまう恐れがつきまとう。
    それを避けるために、全ての手続きを追いかける必要性をプログラマに課してしまい、その行き着く先は1メソッドに全て記述する巨大な手続きだ。
    多くのプログラマが全力で嫌がるCOBOLバッチの世界へようこそ…。
  4. 常にコード内でオブジェクトの状態を確認しなければならない。
    メソッドによってオブジェクトの状態が変わるという事は、メソッドの呼び出し順序にすら制限しかねない不安定なものになる。
    次に待っているのは、その状態をフルコントロールするための状態確認処理の増加だ。
    それはビジネス的に本質的なものではなく、可読性を損う。

つまり、状態を持てば持つほど、それを使用するプログラムはそのコントロールに手間を裂く必要に迫られ、複雑化を招いてしまう。
これは保守性とは相容れない。

どう避ける?

私はこの問題に対し、 「クラス変数を使わない」 という方針をとる。
代わりに使用するのが 「クラス定数」 つまり、 コンストラクタによる初期化以外の値の変更を拒絶する 方針だ。

こうすれば、コンストラクタによってクラスの振る舞いは確定されるので、下記のようなメリットがある。

  • テストが書きやすい
  • 気軽にメソッド引数に渡しやすい
  • メソッドの呼び出し順序生が発生しにくい(DB など、外部入出力があるとまた少し違いますが)
  • 自分のメソッド内(もしくは自分のクラス内)で初期化したオブジェクトの振る舞いは固定なので、振る舞いを当てにしたコードを安全に書ける

欠点も挙げないと不公平でしょうから、欠点も挙げておきます。

  • 別の状態のオブジェクトを作るために、new が必要となってしまう。
    最近の Java/C# のランタイムは優秀で、変数を使わない場合の new コストはかなり低くなってきてはいる。
    しかし、ハードウェアの制約、ゲーム業界ほどの性能厨だと流石に考慮せざるを得ない。
  • 状態で物事を考えるという考え方を捨てなければならない。
    この考え方はどちらかと言えば「関数型言語」の思考に近い。
    慣れるには時間がかり、作るにも頭を働かせる必要がある。

しかし私はこの学習コストを必要悪だと考える。
理由は現在発生中のパラダイムシフトだ。
C++/Java は後発の方だが、F#, Scala, Rust, Kotlin などのように「オブジェクト指向 & 関数型」のハイブリッド化が時流だ。

いずれやらざるを得ないのなら、諦めてやるべきだ。
仕事が先細っていいなら構わないが…