自明なコードを書く
今日は、自明なコード。つまり読みやすいコードについて話をします。
設計で読みやすいコードにすることもできる例としてはDDDがあります。
※先日もちょっとDDDについては触れました。
http://pooh3-mobi.hatenablog.jp/entry/2018/04/20/020654
もっと基本的な部分、たとえばJavaでいうメソッドの中のコードについてはどうでしょうか?
割とおざなりになっているか、リーダブルコードという書籍を基準に考えられている方が多いのではないでしょうかね。
今回は、おそらくそういった部分ではカバーしきれていない部分について、重箱の隅を楊枝でほじくる感じで書いてみます。
自明なコードを書く
自明なコードを書くにはいくつかポイントがあります。
その中の一つに「暗黙知を紛れ込ませない」というテクニックがあります。
暗黙知とはなんでしょうか?
いきなり暗黙知といわれて何かわからないですよね。
この場合の暗黙知は単純なプログラムのコードでは、書き表しきれない、隠れた業務ロジックのことです。
たとえばこんなやつ。
void showContent(List<String> fetchedData) { String str1 = fetchedData.get(0); String str2 = fetchedData.get(1); System.out.println("header:" + str1); System.out.println("body:" + str2); }
これではなぜ0がheaderで1がbodyなのかわかったもんではないですよね。
次のコードのように少しわかりやすく書いたとしても、深い考察がないとこれが関の山でしょう。
よく見るJava1年生のコードじゃないでしょうか。もしかしたらJava3年生の人でも書いているかも…
明らかにダサいコードです。
(特定の個人を批判するコードではないです。コードを憎んで人を憎まず!)
void showContent(List<String> fetchedData) { String header = fetchedData.get(0); String body = fetchedData.get(1); System.out.println("header:" + header); System.out.println("body:" + body); }
これでも本当にList
つまり、実装したときにたとえちゃんと狙った表示ができていても、そのあと、明日か明後日か1か月後か数年後にCALLしている側に変更があった場合にあきらかにshowContentにも影響が出てくる可能性があるわけです。そしてよくあるのが、一部だけ変更し、showContentに対しての影響を考慮もなかったために、わりと割と面倒なバグとなって表れるのです。(どんな面倒さかは、みなさんのイメージ力に任せます、今回の話のスコープ外です)
ではどうすればいいでしょうか?
次に挙げるコードは、この問題を少し改善したものです。
void showContent(String header, String body) { System.out.println("header:" + header); System.out.println("body:" + body); }
一見ですが、このコードは良いコードになったように見えます。
なぜなら、このメソッドは完全に余分な知識を除外しているからです。
使う側に任せれば、こっちは言われたものを表示するだけ。
呼ぶ側の変更はすべて呼ぶ側に押し付ければいいのです。
隠れた業務ロジックはここにはない。自明で読みやすい、暗黙知のないコードです!すばらしい!
なんて、そんなコードがいいわけがない。
わかっていると思いますが、まずダメなのはStringであることです。最初のコードはまだ引数の制約が多少存在しているのでマシでした。
しかし今回はより多く使われるStringです。Stringなら何でも入れられます。Nullも可能でしょうが、今回はNullの話はスコープ外として話します。
そう、これではshowContent側は本当の意味で自明的にHeader、Bodyを表示すると保障していません。
ではどうすればいいでしょうか?
たとえば次はどうでしょうか?
void showContent(List<String> fetchedData) { String header = extractHeader(fetchedData); String body = extractBody(fetchedData); System.out.println("header:" + header); System.out.println("body:" + body); }
これなら幾分かはマシにみえるかもしれないですが。まだ駄目です。
先ほどから繰り返しているように、これだとメソッドをCALLする側の変更があると、見えにくい影響となってしまいます。
次にこれだと・・・
void handleResultData(List<String> fetchedData) { String header = extractHeader(fetchedData); String body = extractBody(fetchedData); showContent(header, body); } void showContent(String header, String body) { System.out.println("header:" + header); System.out.println("body:" + body); }
まぁ多少はマシになりましたが、これでも呼ぶ側が引数のどちらがHeaderで、どちらがBodyか?を確認するために、メソッドの定義を見に行く必要がありエレガントではないです。
僕の場合は、逆算思考を僕はよく使います。どうすればより自明な(=読みやすく、余計な考慮が不要になる)コードになるかを考え、まず実装がなくてもコードとして書いて、あとで今は存在していないクラスを作成しようとします。
実装がないコードを書いている段階で、気に入らなければすぐにぶっ壊して、次のコードを探します。
ではこの場合の自明なコードとは何でしょうか?
void showContent(Content content) { System.out.println("header:" + content.header); System.out.println("body:" + content.body); }
僕が考えるのはこんな感じでしょうか?
まだ幾分か議論すべきことがありますが、この時点で見てわかる通り、このコードは簡潔であり、自明です。
そして、ここでわかるポイントが3つ。
1.暗黙知のあるコードを自明にしようとするとクラス、インタフェースを新しく作る必要がある
2.型の制約により呼ばれる側に強制力を持たせることができ、メソッド単体で見たときに業務ロジック上このコードは安全だとわかる
そしてもうひとつ
3.2をちゃんと安全にするには1のクラス設計時に、安全に使われるように考慮しなくてはいけない
おっと。ちょっと面倒です。ファイルも増える。
これなら最初のコードのほうが幾分かマシなんじゃないか??変更する人の技量やコメントで何とかすればいいんじゃ?
とすらなる事もあると思います。
しかしこれは重要なサインです。何のサインかというとクラス設計に問題がある可能性があります。
面倒なのは「後から変更するときに余計な考慮が必要になるコードを最初から紛れ込ませていた」事による発生する変更コストです。
最初はクラスなんて不要と考えていたコードもつきつめていくと、このような変更が必要になる事が紛れ込みます。
ではどうやって実装すればよかったのでしょうか?どこまで設計を見直せば、このコードを含めすべてのコードが適切な粒度と結合度をたもちながら、自明かつ読みやすいコードになったでしょうか?
それは・・・
わかりません・・・。
残念ながら一度作ってしまって、後からわかるような設計の考慮もれや変更に簡単に対応できるコードの書き方を、レビュー以前の最初からできることはほとんどあり得ないのです。
経験によって、前も同じような問題が発生してこの時の対応をそのまま適応するのが関の山でしょう。
こういったコードに紛れ込む、多くの問題を解決するような方法やツールを、しばしば銀の弾丸と呼びます。
そしてそれは、簡潔な答えはあり得ないことを暗示する言葉でもあります。
ここで紹介したやり方は、解決方法の一つです。しかしいくつかの面倒さを抱えていました。
結局のところ、どの位置にポジショニングをとって、そのコードを書いていくのか?という話になります。
そしてそのポジショニングは開発のスピードやフェーズによって異なるでしょう。
JavaからKotlinに移行を考える
べ、別にKotlin推しとかそういうんじゃないんだけど…Kotlin推しだと思わないでくださいね!
Kotlinは変更を前提にしたコードスタイルに適しています。
後からどんどん変更を入れるというのに適している言語の一つでしょう。
たとえば同じファイルの複数のクラスを宣言できます。これは先ほどのContentクラスを作成するのに躊躇がなくなります。
さっと書いてシンプルにしてしまえばいいのです。
class Content(data: List<String>) { val header: String by lazy { data[0] } val body: String by lazy { data[1] } init { require(data.size >= 2) } } fun showContent(content: Content) = println("header:${content.header}\nbody:${content.body}")
Kotlinはいくつかの問題を簡潔に解決しています。まずファイルを余計に増やすことはしません。またKotlinの言語仕様によってContentの実装がとても簡潔で済みました。
dataのGCがかかるタイミングが気になるのであれば以下のようにも書けます。
class Content private constructor(val header: String, val body: String) { companion object { fun create(data: List<String>): Content { require(data.size >= 2) return Content(header = data[0], body = data[1]) } } }
また、このほかにもNullチェックの冗長な記述がなくなったり、@Nullable/@NonNullを一々入れずに、変数の宣言時の型の情報に"?"をつけるかつけないかで静的解析ツールを使う以前でNullのチェックをIDEレベル行えるのも大きいですし、Lispのマクロほど強力ではないのかもしれませんが言語拡張機能によって既存のAPIに新しく関数を増やしたように見せ、Javaでは不可能だった冗長なコードを見た目上ネイティブをたもちつつ圧縮することもでき、既存Java標準APIの利用によるコードの冗長さを排除できる仕組みが備わっています。
また、関数型プログラミングのエッセンスも取り入れられているので、小さく作って組み上げていく。HowだけではなくWhatを表現するコードが簡潔に書けるようになっている。この利点はプロダクトが成長していく上では重要な利点になりえるかもしれません。
Kotlinの学習が苦だとおっしゃられる方も多少はおられますが、簡潔で自明なコードを実践していきたい、もしくは多くの後から変更を楽にできるようにしたいという考えをお持ちの方は、Kotlinは学習してみるのに十分価値のある言語だと思います。
さいごに
なぜか途中からKotlinを賛美していましたが、簡潔で自明なコードをJavaの力だけで積極的に採用していくには、いくつかの困難があり、Kotlinはそれを簡潔に解決する仕組みが備わっている事は知っておいても損はないと思います。ただこのブログで残念なのは、Javaで簡潔で自明なコードを書くテクニックについて、明解で気軽な方法をしめせなかった事です。今後その部分についてネタがいくつかパターンとして蓄積できれば紹介できればと思います。
最後まで読んでくださった方、ありがとうございます。