名前のルール
一つの処理が長くなってくると、読むのが大変になってくるので、処理を分割する必要が出てくる。処理をまとめて一つの名前をつけることで、読みやすく分り易いコードになる。
複数箇所で同じ処理をしている場合、そこをメソッドとして抜き出すことでムダを省くことができるが、そこに適切な名前を付けられなければ可読性が損なわれる。
無駄な行を減らすことより可読性を保つほうがはるかに重要なので、その処理に適切な名前をつけられない場合は、その処理をひとつのメソッドにまとめてはいけない。その処理を2つの名前で表せるなら、2つのメソッドに分割し、呼び出し側は2つのメソッドをいちいち呼ぶべきだ。
状態(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を経由する。
タイトル未定
前回までのまとめ
そのコンポーネントが目的とする処理を実現するには、正しい順番で必要なメソッドを呼び出していく必要があり、呼び出し順序はオブジェクトの木で規定される。オブジェクトの木の各オブジェクトは重複することがなく、各オブジェクトは自分の子だけに依存する。
ソフトが実現したい最終的な目的を果たすために、汎用のコンポーネントをオブジェクトの木に組み込むことになる。依存性を切断された汎用目的のコンポーネントに依存性を注入し、特化したものをオブジェクトの木から利用する。
汎用目的のコンポーネントにも自分がなすべき処理があり、その処理を実行するために自分のオブジェクトの木を持っている。
今後の話
その処理が一度に全てが実行される類のものなら、状態を持つ必要はないが、GUIなどを用いて、フィードバックを行いながら複数回に分けて実行される可能性がある場合、オブジェクトは状態を持ち、状態を書き換えながら実行される必要がある。
状態の書き換えは問題をやたらと複雑にするので出来る限りやらないで済ませるべき。
一般に時間のかかる処理は、進捗状況をその都度報告してもらい、途中でキャンセルできる必要があり、マルチスレッドで実行を途中で止める仕組みか、複数回に分けて少しずつ実行する仕組みを入れなくてはいけない。マルチスレッド実行を途中で止める仕組みの方が状態を持たなくて済むので良い。
途中まで実行された状態を見せる必要があるのは、移り変わる何かのシミュレーションであったり、アニメーションであったり、移り変わることが前提の処理になる。移り変わる処理では状態をもたせるほかない。
なので、「時間のかかる処理」「移り変わる処理」では状態をもたせる必要がある。他では状態をもたせる必要はない。いままで状態を持っていたものを一つ一つ切っていく必要がある。
移り変わる中でユーザーからの入力を受け付け、移り変わり方が変化することもある。単に時間がかかるだけの処理のように、移り変わり方が一通りであればコルーチンのような言語機能を使った単純な記述も可能であるが、変化する場合はもっとややこしく、オブジェクトの木のどこから処理が開始されるかわからなくなる。GUIを処理すると、このややこしさとと付き合うことになる。他にもアニメーションはコルーチンで書きうるけれども入力に反応するゲームは書けない。
なので処理の開始地点によって分類できる。
単純処理:オブジェクトの木のルートから始まる。
一方向の処理:コルーチンの中断地点またはスレッドの今実行している箇所から始まる(始まる?)
分岐する処理:登録しておいた場所のどこかから始まる。
その話を流れに沿って書いていく。
オブジェクトの木を行き来するもの
前回オブジェクトの木を導入したことで、オブジェクトの依存関係は制約を受けることになった。オブジェクトの木は重複できないので、そのルールに厳密に従えば、2つのオブジェクトから使われるようなオブジェクトは存在できない。
それは全オブジェクトに適用されるわけではない。データはやり取りしなければならないので最低限2つのオブジェクトから使われる。オブジェクトの木は処理の順序を規定するもので、データは処理をしないのでオブジェクトの木に含まれない。逆に言うとデータは処理をしてはいけない。
処理は一般にこのような形になる。
OutputDataType 処理(InputDataType1 input1, InputDataType2 input2){...}
注:inputを最小にする、とか暗黙の第一引数、といったことについて、どこかに説明をねじ込まなければいけない。
InputDataTypeやOutputDataTypeはオブジェクトの木に含まれないので、処理をしてはいけない。しかしデータタイプと言っても、例えばコレクションであればコレクションを扱う各種のメソッドを持っている。そういった汎用の処理は行える必要がある。
定義する。ソフトの目的のための処理を行うオブジェクトはオブジェクトの木に所属しオブジェクトの木の依存関係のルールに従う。データはソフトの目的に依存した処理を行わず、汎用の処理だけを行う。データはオブジェクトの木のオブジェクトに依存しない。
オブジェクトの木の各種メソッドがデータをやりとりすることで処理を行うことになる。ところで、メソッドの基本ルールとして、引数は書き換えないというものがある。戻り値も書き換える必要はない(普通は書き換えようという発想にならないので注意する必要もない)。
なのでデータを書き換える必要はない。定義を付け加える。データは書き換えない。よって書き換えるようなデータはオブジェクトの木に所属させなければならない。
データは書き換えない
JavaやC#ではコレクションのインターフェースにAddメソッドのような、コレクションの内容を変化させて書き換えるメソッドがある。配列も書き換えが可能である。
その昔、配列の変換にも苦労した時代があった。OldType型の配列からNewType型の配列に変換する場合、このようなコードを書いた。
NewType[] Hoge(OldType[] oldArray) { NewType[] newArray = NewType[oldArray.Length]; for(int i = 0; i < newArray.Length; ++i) { newArray[i] = ToNewType(oldArray[i]); } return newArray; }
なので配列は書き換え可能である必要があった。
今はこのように書かれる。
oldArray.Select(old => ToNewType(old)).ToArray();
ラムダ式を使ったコレクションライブラリがあれば、通常の使用では書き換えメソッドを呼ぶ必要はない。
このように、初期化がうまくできないので、あとから初期化するためにパブリックなセッターなどの書き換えメソッドが必要になることがあったが、ラムダ式のおかげで必要なくなった。標準でついてくるAddメソッドやパブリックセッターなどは無用の長物だ。
なのでデータクラスを自作する場合は書き換えメソッドを最初から用意せず、昔からある古い設計のコレクションを使う場合は、書き換えメソッドは使わないようにするのが良い。
メソッドとデータ
依存性(3)
概念整理(2)
ソフトがその目的を達成するためには、必要な処理を正しい順番で過不足なく記述する必要があります。オブジェクト指向言語でそれを記述すると、処理の順序はオブジェクトの木に置き換えられます。
オブジェクトは依存するオブジェクトのメソッドを呼び出します。前回循環する依存関係を無条件で排除したので、依存関係は循環せず、オブジェクトは複数のオブジェクトに依存し、依存するオブジェクトもまた複数のオブジェクトに依存する、という木構造の形をとります。
依存性の木はどのように構成されるべきでしょうか。
依存性の囲い込み
class Bに依存するclass Aがあったとします。BにはMainメソッドから呼び出されるメソッドと、Aから呼び出されるメソッドがあります。
class B { public void FromA(){...} public void FromMain(){...} } class A { B B; public A(B b){ B = b; } public void Hoge(){ ... B.FromA(); ... } } void Main() { B b = new B(); A a = new A(b); a.Hoge(); b.FromMain(); }
MainがA,Bに依存し、AはBに依存しています。この設計で問題なのは、Bが複数箇所に現れていることです。
Main-A-B -B
Bが2箇所に存在していると、木の構造が複雑になり、Bに対して誰がアクセスする責任を持つのかわかりにくくなります。複数ヶ所からいじられれば意図した状態を保つことも難しくなります。
よって依存性の木においてオブジェクトの重複は禁止する必要があります。なので出来る形は2つです。AからBへの依存性を切断してMainにBを持たせるか、AがBを囲い込んでMainからBへの依存性を無くすかです。
今回は依存性を囲い込みます。依存性を切断するか囲い込むかは、依存先のメソッドを別のメソッドに置き換える必要の有無で判別します。
class A { B B; public A(){ B = new B(); } public void Hoge(){ ... B.FromA(); ... } //FromMainだけを露出させる。 public void FromMain(){ B.FromMain(); } } void Main() { A a = new A(); a.Hoge(); ... a.FromMain(); }
Aが依存性を囲い込み、MainからBへの依存性をなくすことができました。(注)
注
ところで話が飛びますが、コンストラクタでnewするのは良くない設計です。
public A(){ B = new B(); }
外部から入れるようにするだけで汎用性が増し、依存性がコンストラクタに現れるので外部からも分かりやすく表現できます。
public A(B b){ B = b; }
呼び出し側はこうなります。
void Main() { A a = new A(new B()); ... }
MainからBのコンストラクタを使っていて、Bにいくらか依存してしまっていますが、実は初期化の時にはどうしても依存性が出てきてしまいます。
なので依存性の木のルールは初期化時には適用できません。初期化時には好きなだけnewを呼び出してコンストラクタに入れてください。それが終わったら触らないようにしましょう。
ちなみに、どうしてもこのオブジェクトの状態はこれでなければダメだ、というのがあるならコンストラクタでnewし、外部には触らせないようにしてください。
public A(){ B = new B(3.1415926535); /*どうしてもこれで初期化しなければダメ*/ }
依存性の木
classがなぜそのメンバを持っているのか、そのclassがなぜ作られたのか、という問いにはいろいろな答えがありえます。状態をprivateフィールドに隠蔽してコントロールするためにclassが必要になったり、多態性のために必要になったり(ラムダ式の登場以降ほとんど必要なくなりましたが)、クラスが大きくなりすぎて見通しが悪いので単にソースファイルを分割するためにクラスが必要になったりします。classは恣意的に、適当に構成されるものです。
依存性の木によってメソッドの呼び出し順序はある程度規定されますが、メソッドの呼び出し順序のためだけにクラスが構成されているわけではないので、依存性の木からはみ出した順序での呼び出しが必要になることもあります。Bのメソッド、FromMain()はAからはみ出しています。こういったハミ出しは大きいより小さいほうが良いですが、それよりも状態を隠蔽する方がずっと重要なので、無理にハミ出さないようにする必要はありません。
大きくはみ出す例を見てみます。依存性の木がこのように構成されているとします。
Main-A-B -C-D
MainからA-Bという木とC-Dという木が生えています。
DからBのメソッド、FromDを呼び出したいとします。
class A { B B; public A(B b){ B = b; } ... } class B { public void FromD(){...} ... } class C { D D; public C(D d){ D = d; } public void Hoge(){ ... D.Hoge(); ...} } class D { public void Hoge(){ /* B.FromDを呼び出したい */ } } void Main() { A a = new A(new B()); C c = new C(new D()); c.Hoge(); ... }
一番悪いのは生のBを渡すことです。AとDの2箇所にBが繋がってしまいます。DがBのパブリックメンバ全てへのアクセス権を得てしまう上に木の構造も複雑化します。
class D { B B; public D(B b){ B = b; } public void Hoge(){ ... B.FromD(); ... } } void Main() { B b = new B(); A a = new A(b); //生のbをDに渡す C c = new C(new D(b)); c.Hoge(); ... }
一番良いのは、メソッドの引数としてfromDを渡す形です。
class D { public void Hoge({void => void} fromD){ ... fromD() ... } } class C { D D; public C(D d){ D = d; } public void Hoge({void => void} fromD){ D.Hoge(fromD); } } void Main() { A a = new A(new B()); C c = new C(new D()); c.Hoge(() => a.fromD()); }
この場合はB.FromD()メソッドがAにはみ出し、C、Dと流れています。依存性の木を伝って長い距離をはみ出していく様子が処理を辿るとわかります。
ただし、呼び出し側でfromDを引数として与えられない場合も考えられます。GUIを用いたイベント駆動のアプリケーションでは、始点がMainではなく様々な場所から始まりうるので、たとえば始点がCになったときfromDメソッドを取得できません。
そういった場合には、Dにフィールドとして持たせる必要があります。
class D { {void => void} FromD; public D({void => void} fromD){ FromD = fromD; } public void Hoge(){ ... FromD(); ... } }
まず悪い設計を書いておきます。
void Main() { B b = new B(); A a = new A(b); C c = new C(new D(() => b.fromD()); }
b.fromD()を直接Dのコンストラクタに渡しています。こうすると、FromDをAにはみ出させる必要も、Cを経由して渡す必要もありません。
この場合の問題は、Dの初期化のためにBのインスタンスが必要になることです。この例ではMain関数ですべてのオブジェクトを初期化するようになっていますが、オブジェクトが増えてくればそのうち破綻し、分割する必要が出てきます。自然に分割すると、自分の子のオブジェクトの初期化に責任を持つ形に分割されるでしょう。
その際に、Aまではみ出させてCのファクトリーメソッド等に流しこむ必要性が出てきます。
void Main()
{
A a = CreateA();
C c = CreateC(() => a.FromD());
}
木に沿って親の方向にはみ出させ、子の方向に流しこむようにしておくことで、そのメソッドがどこで使われているのかわかりやすくなります。
依存性の切断の一般的手法
class Aがclass BのメソッドMに依存しているとします。AからBへの依存関係を切断する一般的手法をコードで示します。
状況はこのように表せます。
class A { public void Hoge() { ... new B().M(); ... } } class B { public void M(){...} }
引数がなく戻り値がない関数型を{ void => void }のように表すことにします。int, stringを引数に取りdoubleを返す関数型なら{ int, string => double }です。
AからBへの依存性を切断するとこうなります。
class A { //引数がなく戻り値もない関数を受け取る public void Hoge({ void => void } m) { ... m(); ... } } class B { public void M(){...} } void Main() { A a = new A(); //B.Mを与える a.Hoge(() => new B().M()); }
AからBを直接呼び出さず、Aの呼び出し側からBへの依存性を注入してやることでAからBへの依存性を切断することができます。
ただし、この切断が必要になるのは、AからB.M()以外を呼び出す可能性がある場合です。class Aの目的上、どう考えてもB.M()以外を呼び出すことがないなら、関数型にしてラムダ式でB.Mを与えてやる必要もありません。そういう場合は、後述する「依存性の囲い込み」を行ってください。
B.M()以外を呼び出すならば、AからBへの依存性を切断することでより汎用的に使えるようになり、再利用性が高まります。
依存性の循環とカプセル化
B.M()以外を呼び出すことがないような、汎用性が必要ない場合でも、経験上依存性を切断したほうがいいこともあります。それは依存性が循環する場合です。
class AがBのメソッドMBに依存し、class BがAのMAに依存するとします。AとBがお互いに依存しあい、依存性が循環しています。
class A { public B B; public void MA(){ ... } public void Hoge(){ ... B.MB(); ...} } class B { public A A; public void MB(){ ... } public void Hoge(){ ... A.MA(); ... } } void Main() { var a = new A(); var b = new B(); a.B = b; b.A = a; a.Hoge(); b.Hoge(); }
クラスA,Bがお互いにお互いをフィールドとして持ち合う形になっています。こういう場合はコンストラクタで初期化することが困難です。
無理やり書こうとすると、こんなふうになります。
var a = new A(new B(a));
まだ初期化されていないaをBのコンストラクタに突っ込もうとしてコンパイルエラーを引き起こしてしまいます。なのでpublicにして後で代入していますが、これは必要のないメンバの露出でありカプセル化が損なわれています。
また、AはB.MB()に、BはA.MA()に依存していますが、A.Hoge()、B.Hoge()には依存していません。なのにAはBをフィールドに所持しているので、B.Hoge()にアクセスすることができます。Bも同じです。
クラスは一般的に大きくなりすぎ、クラスのインスタンスを渡すと必要のないメンバへのアクセス権まで渡してしまうことが多くあります。この場合はHogeメソッドが余計なので、Hogeメソッドを別のクラスに移すことで対応が可能かもしれません。しかし、そんな場当たり的な対処をする必要は本来ありません。必要なメンバへのアクセス権を必要なだけ注入してやれば良いのです。
class A { public void MA(){...} public void Hoge({void => void} m){ ... m(); ... } } class B { public void MB(){ ... } public void Hoge({void => void} m){ ... m(); ... } } void Main() { var a = new A(); var b = new B(); a.Hoge(() => b.MB()); b.Hoge(() => a.MA()); }
AとBの依存性を切断し、余計なメンバへのアクセスも遮断することができました。一般に、必要なメソッドがある場合は、関数型にして外部から入れてもらうことで依存性が切断できます。
今回は必要ないですが、フィールドに持つ場合も書いておきます。
class A { {void => void} M; public A({void => void} m){ M = m; } public void MA(){ ... } public void Hoge(){ ... M(); ... } } class B { {void => void} M; public B({void => void} m){ M = m; } public void MB(){ ... } public void Hoge(){ ... M(); ... } } void Main() { B b = null; var a = new A(() => b.MB()); b = new B(() => a.MA()); a.Hoge(); b.Hoge(); }
循環しあって初期化できなかったものが、ラムダ式の魔法を使うことで初期化可能になりました。
B b = null; var a = new A(() => b.MB());
「() => b.MB()」このラムダ式が実行されるのは、a.Hoge()を実行した時です。つまり
b = new B(() => a.MA());
この初期化が行われた後になりますから、ヌルポインターエクセプションは起こりません。
依存関係を切断しよう
処理が別のライブラリの処理を呼び出す場合、処理はそのライブラリに依存しています。
処理には入力と出力が必要です。入力データ型や出力データ型が基本型でなく、特別に作られたものだったなら、処理はそういったデータ型に依存することになります。コードで書くとこんな感じになります。
OutputDataType 処理(InputDataType input){ ... }
処理と入力データ型、出力データ型のように密接な関係にある場合、依存関係を分断することは不適切です。それらはむしろ一つの塊として考えてください。
これから処理やデータ型をひとつにまとめたものを「パッケージ」と呼ぶことにします。パッケージの内部にはそれぞれ密接に関係した型がまとめられています。問題にすべきはパッケージの間の依存関係で、依存性をできるだけ小さくすることで処理の再利用性を高め、複雑に依存しあった処理を解きほぐし理解しやすくすることができます。
依存性を注入しよう
一例としてこのようなコマンドを解析することを考えてみましょう。
makevideo -b 600 source.avi dest.mp4
makevideoコマンドは動画ファイルを作るコマンドで、-bの後に数字を入れると動画のビットレートを設定することができます。-bの他にも様々なオプションがあるものとします。
説明のためにC#によく似たコードを使っていきます。
コマンドラインの文字列を解析するために、とりあえずAnalyzeというメソッドに投げることにします。
//argsにはコマンドラインの文字列が入っています void Main(string[] args) { Analyze(args); ... }
Analyzeの中には、それがオプションであるかを判定し、オプションによってswitch文で分岐する、オプションじゃなければムービーを作成する、そんな処理を書きました。
void Analyze(string[] args) { for(int i = 0; i < args.Length; ++i) { string optionStr; if(IsOption(args[i], out optionStr)) { switch(optionStr) { case "b": ...//ビットレートを設定 break; case ... } } else { CreateVideo(...); } } }
この処理は、「-bの場合ビットレートを設定する」というようなソフト個別の事情に基づいているので、他のソフトで再利用することが出来ません。また、CreateVideoというメソッドを呼びだしていますが、このメソッドはソフト独自のライブラリに含まれるメソッドです。ソフト独自の動画作成メソッドに依存することにより、動画作成以外の用途での使用ができなくなっています。
余計なパッケージに依存するから悪いのです。こういった依存性は切断しましょう。
オプションのデータだけを解析して返し、それを使って何をするかは呼び出し元に任せることにします。
//-b 600の場合、OptionStr="b", Arg="600" が入る。 class OptionArg{ public string OptionStr; public string Arg; } //オプションはOptionsに、変換元、変換先ファイル名はFileNamesに入る。 class CommandLineArgs{ public OptionArg[] Options; public string[] Args; }
このようなデータ型を作り、Analyzeを呼び出すと、{"b","600"}というように整理された文字列が返ってくるようにします。
void Main(string[] args) { CommandLineArgs cl = Analyze(args); foreach(OptionArg option in cl.Options) { switch(option.OptionStr) { case "b": //ビットレートを設定する } } ... }
さて、Analyzeメソッドはコマンドラインの整理だけをするようになり、ソフト個別の事情から自由になったので、他のソフトからも再利用可能になりました。多少マシになった感はありますが、switch文の煩雑さなど見るに、余計な処理を書いているように見えます。
なぜこのように煩雑になってしまっているのかというと、Analyzeメソッドがソフトの個別の事情を教えてもらっていないからです。仕方なく一般的な文字列だけで結果を返しているので、文字列をオプションに変換する煩雑な処理を書く必要が出てきます。
再利用可能になるように、ライブラリは個別のソフトの事情に依存しないように書く必要がありますが、、ライブラリを使うときには、個別のソフトの事情を教えてやることで余計な処理を書かずにシンプルなコードにすることができます。あとから依存性を注入してやれば良いのです。
まずOptionData型というのを作ります。
class OptionData{ public int Bitrate; ...(などなどいろいろなオプションが並ぶ) }
ビットレートやなんかを保持しておく型です。AnalyzeメソッドにOptionData型のことを教えて、適切に扱えるようにする必要があります。AnalyzeメソッドにOptionData型への依存性を注入するのです。
たとえば、-b 600といったデータがあったら、OptionData型のBitrateメンバに600を入れれば良い、ということを教えていきます。
OptionData data = new OptionData(); Analyze( Case("b", arg => data.Bitrate = int.Parse(arg)), Case(...),... );
「arg => data.Bitrate = int.Parse(arg)」
これはラムダ式です。argにはオプションの引数が入ります。たとえば-b 600の場合は"600"という文字列が入ります。それをint.Parseで整数に変換して、Bitrateに代入しています。
ラムダ式がまだ導入されていない言語の場合、このように書けば大丈夫です。
interface OptionAction{ void Action(string arg); } class BitrateAction : OptionAction { OptionData Data; public BitrateAction(OptionData data) { Data = data; } public void Action(string arg) { Data.Bitrate = int.Parse(arg); } } ... Analyze( Case("b", new BitrateAction()), ...
ただラムダ式が導入されていない言語の場合、冗長で見通しの悪いコードになってしまいがちなので、依存性の注入はやらない方がベターな場合も多いです。
さらに、CreateVideoメソッドへの依存性も注入してやります。
OptionData data = new OptionData(); Analyze( Case("b", arg => data.Bitrate = int.Parse(arg)), ... args => CreateVideo(args, data) )
ソフトウェアの構造を知ろう
再利用できる部分とできない部分を知ろう
ソフトはだいたいの場合、何らかの目的を持っています。その目的を達成するために、計算をし、結果を画面に表示したり、ファイルに書きだしたりします。
結果を画面に表示したり、ファイルに書きだしたりする操作は、戻り値を持ちません。ソフトの実行結果は、戻り値を持たないので、基本的に再利用できません。全く同じ目的を持つソフトなら、再利用することで目的を達成できますが、目的が少しでも異なる場合、そのソフトを直接再利用することはできません。
再利用する場合は、「画面に書き出すためのデータを計算する処理」や、「ファイルに書き出すためのデータを計算する処理」をライブラリ化し、それを使って計算結果を戻り値として取得して、それを加工することで目的とする処理を実行することができます。
つまりソフトは、「再利用できるライブラリ層」と「ライブラリを呼び出す再利用できない層」に分けることができます。たとえば、最上位にあるmain関数は再利用できません。main関数から呼び出されるライブラリは再利用できます。
再利用のレベルを分類しよう
ソフトは、再利用できない上位層から、再利用できるライブラリを呼び出すことで目的とする処理を実行します。ライブラリには様々なものがあります。
- ソフトの一箇所から利用されている処理
- ソフトの複数ヶ所から利用されている処理
- 別のソフトウェアからも利用されている処理
再利用のされ方によって、処理をどのくらい汎用的に書けばいいかが決まります。
再利用されない処理は、全く汎用性をもたせる必要はなく、目的のために必要な最低限の処理だけを書けば十分です。目的のために必要な処理だけをpublicにし、必要のないものはすべてprivateにします。
逆に、別のソフトからも利用される処理は、今必要がなくても、必要になりそうな処理はすべて書いてpublicにしておく必要があります。
その中間にあるのが、一箇所から利用されている処理、複数ヶ所から利用されている処理です。これらは別のソフトからは使われないので、今のソフトの目的に必要なものだけを書けばいいように思うかもしれません。
しかしソフトの目的は移り変わりますし、新機能を搭載したくなることもあります。そういった仕様変更に対応できるようにするには、できるだけ処理を汎用的に書いておく必要があります。
ただし、「汎用的にする必要が出てきてから汎用的にしたって手間は変わらないじゃないか。いつ起きるかわからない仕様変更のために先んじて汎用的にしておくなんて馬鹿げている」という考え方もあります。私も基本的にはその考え方です。
しかし「その処理の一般形はなんだろうか」と考えてみることは重要です。その処理を適切に汎用化したら、たとえば、二次方程式のために場当たり的に因数分解の公式を当てはめてみるのではなく、解の公式を使うことはできないか。これだけあれば全部含んでしまう一般的な処理があるのではないか。と考えてみることです。
一般形を見出すことが出来れば、仕様変更があっても変更する必要があなくなりますし、場当たり的な複数の処理が一個の一般的な処理に置き換えられれば、記述自体もシンプルにできることが多いです。
しかし、一般化しすぎたために処理が抽象化されすぎて、何をやっているのかわからなくなってしまうこともあります。そういった場合はあえて一般化をせずに、直感的にわかりやすい個別の処理を書いて使った方が良い結果になるでしょう。