状態(1)
状態が必要になる条件
状態が必要になる条件として、処理が途中で中断される場合があるということを昨日書いた。
コレクションや相互参照の初期化のためにパブリックセッターやAddを作る必要はないということも書いた。初期化の問題はラムダ式で解決してる。
効率の問題で書き換えが必要になる場合がある。単語をカウントするコードを書くと、
void Hoge(string text) { string[] words = ToWords(text); //テキストを単語に分割する。 //ハッシュテーブルに指定したKeyがない場合、 //そのKeyの値を自動的に0で初期化するハッシュテーブル。 var dic = new AutoDict<string,int>(key => 0); //各単語が何回出てくるか調べる foreach(var word in words) { dic[word]++; } }
こんな感じになる。これを書き換え無しで効率的に書く方法がちょっとわからない。
この処理は(よっぽどtextが長くて時間のかかる処理にならない限り)一度に実行できるので、書き換え可能なオブジェクトであるdicはローカル変数として使われメソッドの終わりと共に消える。実行結果は、ReadOnlyHashTableみたいなものにして戻せば、状態はメソッドの内部で閉じ込められ外部に漏れない。(ルールでは、コレクションに書き換えメソッドがあっても気にしないことになっているので、そのまま返してもいい)
閉じ込められない状態を持つ場合、つまり状態がコンポーネントの境界を超えて保持される場合、オブジェクトの木の中のどこかのフィールドに状態が保持されていることになる。
コンポーネントの境界と状態:コンポーネントの境界はオブジェクトの木のルートのことを指している。オブジェクトの木は中のオブジェクトを直接露出しないので、コンポーネントを使う側はオブジェクトの木のルートにしかアクセス出来ない。オブジェクトの木のルートにあるメソッドを呼んだ時に、オブジェクトの木の状態が変化するなら、コンポーネントの境界を超えて状態が保持されている。
状態が保持される場合、状態を持つインスタンスはフィールドとしてオブジェクトの木に組み入れられる。オブジェクトが状態を持たない場合、フィールドとして持つ必要はない。その場合さまざまな表現方法があり得る。どれを選んでもいい幾つかの表現方法が。
どれを選んでもたいした差がない場合、一つに決めてしまう方がいちいち悩まなくて済むので良い。文体を変えてみたがしっくりこない。
たとえばこのようにメソッドの中でnewして使ったり
void Hoge() { var a = new A(); a.Hoge()... }
どうせ状態がないのだからとstaticメソッドとして実装したりできる。
void Hoge()
{
A.Hoge()...
}
あるいは、フィールドに持つ必要がなくても、フィールドに持ってしまうという手もある。
前回までにルールとして、クラスが処理をするにはオブジェクトの木に所属させないといけないと決めた。オブジェクトの木は、その場でオブジェクトを作って所属させてもいいし、スタティックメソッドを使うために、クラスの静的な要素を所属させても良い。「同じインスタンスを複数箇所から参照できるようにしてはならない」「クラスの依存関係が循環してはいけない」ルールはこの2つだけだ。
いままで状態という言葉を区別せずに使ってきたが、変化する状態を状態変数、変化しない状態を状態定数とする。状態が定数であれば、データで表現し、メソッドに入力で渡すことができる。
//スタティックメソッドの場合 A.Hoge(data); //インスタンスメソッドの場合 a.Hoge(data);
コンストラクタでデータを入れる方法もある。
var a = new A(data);
a.Hoge();
この中から最も可読性と再利用性の高い最良の記述を選んでそれだけを使うことにする。
メソッドの再利用性
メソッドは一般にこのような形をしている。
ResultType Method(Input1 input1, Input2 input2...)
メソッドが複数の値を返すときには、値を返すためだけの型を作ることが多くある。引数は複数の値を入れることができるので引数の為だけに型を作ることは少ないが、複数の値を持ったオブジェクトのコレクションが引数として欲しい場合など、その複数の値を引数として渡すためだけに型を作ることがある。
ところで、Methodが他のコンポーネントでも使えるような汎用的な処理であっても、引数や戻り値に使われている型が、あるコンポーネントでしか使われないようなものだと、その汎用Methodが使いたいだけのために、関係ないコンポーネントをインポートする必要が出てきてしまう。これは避けたほうが良い。
一般に、その処理に必要な引数や戻り値をまとめた型は、他の処理とは違う。特別な事情により論理的に同じだと断定できる場合もあるが、引数や戻り値のデータをまとめただけのクラスは、処理によって違うのが当然だ。たまたま同じであったために使い回したとしても、メソッドの機能追加で入力や出力を増やしたりすれば使いまわせなくなる。
なので、引数や戻り値をまとめた型は、そのメソッド専用に作り、使いまわすべきではない。型の変換のために処理が必要になることもあるが、それは処理の汎用化のために必要な、支払うべきコストだ。
また、メソッドに処理と関係ない余計な引数を与える必要があると、混乱の元になるので避けたほうが良い。メソッドのために引数、戻り値をまとめた型を作るときは、そのメソッドのためだけの、処理に必要な最小限のデータだけ持った型を作るべきだし、引数のための型を作らない場合でも、使わないものを引数として与えられるべきではない。
ところで、インスタンスメソッドには、「暗黙の第一引数」と呼ばれる引数がある。
ResultType Method(/* MyType this */, Input1 input1, Input2 input2...)
thisポインタは、ソースコードでは隠れているが、実際に実行するときにはメソッドに渡されている。だからインスタンスにアクセスできる。ここではその型を仮にMyTypeとしている。
隠れているけれど引数なので、通常の引数と同じように考える。まず考えるべきは、その処理がMyTypeに依存する必要があるのか、ということだ。
このMethodを呼び出すには最低限MyTypeのコンストラクタを呼び出しインスタンスを作らなければならず、呼び出し側はMyTypeという型に依存することになる。余計な型への依存は防がなければならない。
またMyTypeをデータとしてみると、Methodで使いもしない余計なデータを引数として渡していないかという問題が出てくる。インスタンスメソッドがそのインスタンスのすべてのフィールドを毎回全部使っているということはまずないので、Methodに本来必要のない余計なデータを渡している。これもまた再利用性を損なわせている。
また、MyTypeには様々なメソッドがある。インスタンスメソッドの場合はprivateメンバにまでアクセス権が及ぶが、それらのほとんどもMethodの内部では使われない。使いもしないprivateメンバへのアクセス権が渡っていて、これもまた混乱のもとだ。
インスタンスメソッドはインスタンスメソッドであるだけで様々な無用の関係性を抱え込み問題を複雑にしてしまう。状態をprivateフィールドに囲い込むためにはインスタンスメソッドに頼る他ないのだが、それでもこの複雑さは出来るだけ排除したほうがいい。
- staticメソッドにできるものはstaticメソッドにする
つまり状態変数を書き換えないメソッドはstaticメソッドにする。
- フィールドはできるだけ少なくする
使いもしないフィールドへのアクセス権を出来るだけ渡さないようにするには、フィールド一つに対してクラス1つといったように出来るだけそのクラスが担当するフィールドを少なくして、状態をそこに囲い込み、そのフィールドだけを使うメソッドはそのクラスに配置する。
AとBという2つのフィールドの状態を使うメソッドがあったら、Aを担当するクラスとBを担当するクラスに出来るだけ処理を分けることで問題を簡単にする事ができる。
- クラスはあまり大きくしない
インスタンスメソッドはそのクラスの全privateフィールドへのアクセス権を持っていて、どこから書き換えられているかよく分からず、状態を適切に保つことが難しい。対策として、そのクラスをひと目で見渡せると大きさにまとめておくことで、どこから書き換えられているかわかり易くなる。
なのでクラスがひと目で見渡せない大きさになったら、フィールドのような分割の根拠になるものがなくても、とりあえず分割する必要がある。メソッドの内容をstaticメソッドに丸投げすることで分割するのが有効だ。
static化
インスタンスメソッドをstaticメソッドに丸投げする形にするのはいつでも有効なリファクタリング手法だ。
Method()をstaticメソッドにすることを考える。
class A { int X; void Hoge(){...} public void Method(){ //privateメソッド、Hoge()の呼び出し Hoge(); ... //privateフィールドを使ったメソッド呼び出し var y = Huga.Method(X); ... //状態の書き換え X = Huga.CalcX(y); } ...他にもなにやら膨大に書いてあって見通しが悪い }
staticに直す。staticに直しても状態の書き換えは自分のところで行う。
//分割するために作った適当なクラス class StaticA { static void Hoge(){ ... } //処理の中身を書く。 public static int Method(int x){ Hoge(); ... //privateフィールドに依存してたところは //引数として渡してもらう。 var y = Huga.Method(x); ... return Huga.CalcX(y); } } class A { int X; //privateメソッドは消える public void Method() { //staticメソッドに丸投げ var result = StaticClass.Method(X); //状態の書き換えは自分でやる X = result; } ... }
処理をstaticメソッドに投げることでインスタンスメソッドは状態の管理に集中できる。
呼び出しの3分類
状態変数を書き換えないメソッドはstaticメソッドにすることに決めたので、呼び出しの見た目から次のことがわかる。
//スタティックメソッド呼び出し
Static.Method()
スタティックメソッド呼び出しは状態を書き換えない。
void Method(Data d) { var hoge = new Hoge(d); hoge.Method(); ... }
ローカル変数にインスタンスを作成して呼び出す。
この場合処理中に状態が変更されているが、状態の囲い込みに成功していて、状態の影響はメソッドの外に漏れていない。
class A { Hoge Field; public A(Hoge h){ Field = h; } public void Method() { Field.Method(); ... } }
フィールドに作成されたインスタンスのメソッドを呼び出している。この場合状態を変更していて、状態変更の影響がメソッドの呼び出し側に伝わっている。
ちなみにルールから、同じインスタンスを他の箇所から使うことはできないので、このフィールドに対する変更は必ずAを経由する。