クラスやメソッド、関数を分割するとき、その中の複雑さは減るが全体の複雑さは増える

タイトルで全てを言ってしまった。

クラスやメソッド、関数が大きくなってきたとき、これを分割したいと思うのは当然のことだと思う。分割統治することでひとつひとつのコンポーネントはシンプルになり、メンテしやすくなる(はず)だからだ。一方、クラスやメソッド、関数の数が増えれば増えるほど、全体としての複雑さは大きくなっていく。「なんかいろんなところに無秩序に飛んでいって読み解きにくいコードだな」と思ったことはないだろうか? わたしは(自分が過去に書いたもの含めて)そういうものを結構見てきた。

ここで「じゃあ全部ベタ書きすればいいね!」となるのはあまりに短絡的なので、もう少し考えてみる。

「なんかいろんなところに無秩序に飛んでいって読み解きにくいコードだな」となるのは、分割したことが単純な原因ではなく、分割の仕方が悪いと考えることができると思う。つまり、その分割によって凝集度が下がっているのではないだろうか。あるいは、「もともとひとつだったもの」を単純に分割するということは「結合度の高いふたつのコンポーネントを作る」ということでもある。分割によって全体性の複雑さが上がるデメリットがあったとしても、そのデメリットを上回りメリットを単体の複雑性を減らすことで得るためには、単純に分割するのではなく、凝集度が高くなり、結合度が低くなるような分割点を見つける必要がある。

では、むしろ凝集度を下げ、結合度をあげてしまうような分割の「コードの匂い」をどのように嗅ぎ取ればいいだろうか。ぼくがいまパッと思いついたのは「引数をいろんなところにひき回している」「インスタンス変数にいろんなところからアクセスしている」という2例がある。

引数をいろんなところに引き回すことによって引数を介した結合度は上がるし、その引数の関心がいろんなところに散らばっているので、これは当然結合度と凝集度が高くなる。結果として全体の複雑性を大きく引き上げることになる。分割したときに引数をひき回しているのを見つけたらなにかがおかしいと思うべきだ。

また、たとえばインスタンス変数を介してメソッドの結果をクラス内でやりとりしている場合、仮にprivate変数であっても、そのインスタンス変数という状態を介して、複数のメソッドが結合することになる。さらにいえば他のあらゆるメソッドがそのインスタンス変数にアクセス可能なわけで、あらゆるメソッドがこのインスタンス変数を介して潜在的に結合してしまっている。たとえばもともメソッド内ではローカル変数に閉じていたものを、分割に伴ってインスタンス変数を介してなにかをやるようになってしまったら「状態を気にしなければならないスコープ」は拡大し、メソッド単位の凝集度が下がり結合度が上がってしまう。publicなインスタンス変数を介したやりとりに至っては、さらにそれがクラス外への影響として出てくることとなる。分割した際にインスタンス変数が増えたらなにかがおかしいと思うべきだ。

なんだか2例とも「変数にアクセスできるスコープはなるべく小さくしよう」というあたりまえの話に収束してきた気がする……。

別の視点の話をする。じゃあどういうときに全体の複雑性が上がったとしても単体の複雑性を減らすべきだったと判断できるのか。これは結構難しい問いだと思うんだけど、上記のようなコードの匂いを感じさせない、きちんとそれぞれに閉じた分割ができたのであれば、それによって上がる全体の複雑さは「そもそもそのビジネスロジックの複雑さ」の表れなので受け入れるべき複雑さとなるのではないかな、というのが今の意見だ。逆にいうと、「そんなに複雑なビジネスロジックじゃないはず」のものがいろんなところにすっ飛んで実現されているのであればそれは「全体の複雑さを徒に上げているだけ」の分割であるとみなしても良いと思う。

凝集度を下げ、結合度をあげてしまうような分割の「コードの匂い」について、他の例もおそらくあげられると思う(のでみんな記事書いてほしい。読むし仕事で参照するから)が、いずれにせよ「単純に分割するだけ」ではむしろ全体の複雑さを増やす場合がある、ということをここでは共有したかったのでした。