読みやすいコードってどんなものか考えてみた -抽象化と名前重要-
あらすじ
人の綺麗なコードを読みまくると自分のコードも綺麗になっていくのに、イケメンを見続けても僕の顔が良くならないのは何故なの??
2012-11-30 19:41:20 via web
今まであまり人のコードを読む習慣というか機会というかがあまりなかったのですが、最近になって、デスクの上がヨドバシのiMac売り場みたいと(僕の中で)話題沸騰中の@mitukiiiさんのコードを読む事があり、この人がまたすごく綺麗でスタイリッシュなコードを書くわけで、その時に、綺麗なコードというのはこういう感じに書くものなのかと結構な衝撃を受けたわけです。
またこれも最近なのですが、別の機会で、なんと言いますか、1つの関数が数千行あったり、しかもその内の大部分が共通処理として括り出せるような恐らくはコピペされたであろう部分が大量に入っていたりまぁ不可解な部分の多い、言うなればイケメンを見続けた僕みたいな、つまりはすごく綺麗なコードを大量に読んでいたりもしていました。
で、そういう今までなかった経験を通して、読みやすい・理解しやすいコードを書くのに必要な事って何だろうかとちょっと考えてみました。
僕の読みやすいコードのイメージ
「コードは綺麗に書くもんだ!」という意識だけは頭の片隅にありはしたものの、マジで片隅にあったので、その意識が表層化したことはほとんどありませんでした。
なので、じゃあどうやって綺麗で読みやすいコード書くんだよ?と自問してみても、パッと思いつくのは「とりあえず、コードは短くかけりゃいいんじゃないのかなぁ...」くらいの馬鹿丸出しな、馬鹿剥き出しな感じの答えだけでした。
じゃあその答えをもっと突き詰めて、ある処理、つまりメソッドや関数が何行以下ならば「短い」と言えるのでしょうか?
例えば、次のようなコードは短くないが故に良くないのでしょうか?
User.where({ :sex => "male", :age => 20..30, :country => "japan", :job => "engineer" })
処理的には1つのことしかしていないけど、6行も使っているから読みにくい・理解しにくいと果たして言えるのでしょうか?
そう言えるとは思えません。むしろ1行に無理やりおさめるよりはこちらのほうが見やすいでしょう。
じゃあ行数はいいとして、処理的に少なければ良いってこと?そしたら共通化で重複する部分がなけりゃ最強に読みやすいってわけ?それだけで本当に理解しやすいコードが書けるの?
コードを凝縮するための技は「共通化」しか知らない僕が、自分で書いたコードを書いた2日後に見て「なんだよこのクソコード...」と毎回言ってるのを思い出せば、その答えがNOなことは明らかでした。
dry原則に従った共通化
同じような処理をしている部分は関数化して同じコードが存在しないようにするというような、dry原則に従った共通化も単純なコードの行数を減らすことは確かにできます。しかし、コードの行数が減ったからといって、それがコードの見やすさに直結するのでしょうか。
前述の不細工な20代男性をプリコンパイルしたようなアレなコードを読んでいた時は、共通部分の関数化やコメントごとのコピペ行ってるせいでコメントがコードの理解を助けるどころか罠にしかなっていないとか、ドキュメントが一切ないためコピペされていないコメント以外に情報がないとか、僕自身その言語にあまり精通していないとかいうゲリラ戦さながらの状況だったのは確かですが、それが意外にも読み易い部分が多かったことに驚きました。
人のコードを読んだ経験もあまり無く、「そういうコードは保守性やら拡張性やら可読性やら全ての観点においてクソである」というようなそこかしこで見る意見を鵜呑みにしていたため、サッと目を通した時点で色々感じ取って「ああ、このコードはきっと読解に骨が折れるだろうな」とばかり思っていたのですが、そんな馬鹿な僕の期待は裏切られました。
何故そのようなコードを読みやすいと思ってしまったのか?と色々考えてみたところ、黒魔術的なコードが少なかったり、コメントがなんだかんだで理解の助けになっていたりと、コードを読みやすくする要素がその中に盛り込まれていたというのも思いつきましたが、関数やメソッドが一枚岩になっていることが逆に僕にとってはコードを読みやすくしている大きな理由なのではないのかと考えました。
何か1つ関数を読む場合、その関数のなかにさらに別の関数があれば、その関数の中でどんな処理を行なっているか見に行くことが多いと思います。で、その見た先の関数の中を見るとさらに別の関数があってその中を見に行って...と、繰り返していくと、次第に脳内スタックに戻り先が溜まるはずです。最終的に行き着く先まで関数を見た後で脳内スタックを順にポップしていけばコードの深くまで理解した上で読んでいた元に戻れるはずなのですが、それが僕はうまく出来ません。いまどこ読んでたっけ...みたいなことになります。2日前に自分で書いたコードを忘れちゃうような、多摩地区一の馬鹿と呼び声の高い僕には出来ないのです。
そんなもんデバッガのスタックトレースなりIDEの機能使えば、一々脳内スタックにつめ込まんでもエエやんけ多摩地区どころか関東甲信越地方一の馬鹿じゃねえか!という声も聞こえてきそうですが、脳内スタックというか精神的なスタックはそう簡単に戻れるものではないのではないんじゃないかな、と思っています。
で、話が戻るのですが、そもそも共通化の本質は、同じ処理をまとめることでその処理が後々変更になったときの工数的なコストや変更忘れなどのリスク回避なはずです。物理的な行数が減ったところで得られるメリットはコードの可読性ではないです。
上に上げた共通化すべきところをしないというのはかなり極端な例だし、コードを書く上で意識すべきは何も可読性のみではないのはもちろんなので、共通化は必須でしょう。ただそれを何のポリシーもなく、diffとったら重複行があったのでまとめましたみたいな機械的な共通化をしてるだけでは、可読性は上がるどころか下手すると下がってしまうのではないか?と考えたのです。
というか現に今も自分の書いたコードを毎日みながら、共通化してるとは言うけどその共通化部分が書いてある場所があっちこっちに散らばっていて非常に読みづらいし、このコード書いた奴を●してえ...と1日3回はツイッターで呟いている有様です。
共通部分でなくとも抽象化
前述の人間味溢れるコードとほぼ時を同じくして@mitukiii大先生のコードを読んでいて気づいたことがありました。
僕は関数化またはメソッド化する部分は複数箇所で参照されるような共通処理でなければならないと思い込んでいたのですが、@mitukii大納言はコントローラのある1ヶ所でしか参照されないような処理をメソッドとして括り出していたのです。
例えばproductというモデルがあるとして、Railsで使われているActiveRecordというORマッパーを使ってそのデータを取得してみます。
Product.where(:price => 0..5)
これが何をしているかは分かります。productモデルの中で、priceカラムの値が0から5までを取得しているということです。
が、こんなに短い取得処理なのに、それが何を意味しているのか、「priceカラムの値が0から5までを取得している」というそのまんまの事以外はちょっとわかりません。
現実世界における価格という属性をスキーマに落とし込んだのがpriceカラムであろうことはそのカラム名から推測できます。しかし、priceが5以下だった場合に、それがどういうものなのかはこのコードからは読み取れません。0から5までのpriceという事からソレが安いProduct何じゃないかと推測出来るかもしれませんが、「安い商品を取得する」という処理をアプリケーションで使うようなケースというのはちょっと想像しにくいです。
で、これをscopeというActiveRecordの機能を使ってクラスメソッドに書きなおしてみます。
class Product < ActiveRecord::Base scope :need_to_pay_postage, lambda { where(:price => 0..5) } end
これによって先ほどのpriceカラムの値が0から5までを取得するコードは少しだけ短くなり、Product.need_to_pay_postage で表せるようになりました。これの良い点はコード量が少しだけ減ったことではありません。このコードをメソッドとしてまとめる上で名前をつける必要が出てきたため、その文脈における自分自身を説明できるようになったことです。
「need to pay postage」は「送料がかかる 英語」でググって一番最初に引っかかったweblioに載っていた訳ですが、このモデルが実はあるECサイトで使われているものであって、それが5ドルより高い商品については送料無料なことが売りなサイトであったとします。そうすると、このサイトという文脈において5ドル以下の商品は、それが安いとか高いとかというよりも「送料のかかってしまう」という意味合いのほうが強くなるわけです。
このようなものを「抽象化」とか「カプセル化」とか言うのでしょう。
自分自身を説明できるメソッドなり関数であれば、そのメソッドの行う処理を理解するためにわざわざその定義している部分を見なくてもいいはずです。これによって前述の安易な共通化してるせいで結局メソッドなり関数なりの定義元見に行かなきゃ理解できねーしわざわざ見に行くのめんどくせーよ問題(そのまんま)が解決するんではないのかと思いました。
どういう基準で抽象化するんだよ?
共通化したいメソッドは処理まとめた上でいい感じの名前つけて抽象化すればいいし、共通化する必要はなくとも処理の分かりにくいメソッドなら抽象化するだけでもオールオッケーだよねーという感じのことは考えて見ましたが、結局これらをどういう基準で、どのような意味の単位で、共通化なり抽象化すればいいのかはわかりません。
基準がないと、ある処理をまとめていいものかどうか、あっちの処理はまとめたけどこっちの処理もまとめるべきなのかとか、理解しやすいコードを書くためのフェーズでどうまとめたらいいものか悩みそうです。
また、基準がないとまとめる単位の一貫性が損なわれます。一貫性を期待してコードを読む多くの人は、「一見」バラバラの単位でまとめられているけど、そこには何かしらの意図があってあえてこのようにやっているのでは?と深読みするかもしれません。その深読みを徒労に終わらせるコードが、理解しやすいコードとは到底言えません。
何にしても、抽象化の利点は明示的に意味を持たせることにあるわけなので、その意味をうまく伝えられるような単位でまとめるのが良いのですが、どうすれば基準を得られるのでしょうか?
名前重要の俺式解釈
で、そこで基準として扱えるものは「名前」なのではないかと考えました。
例えば「Ruby」という単語の説明文を書けと言われて「まつもとゆきひろという島根県松江市在住1965年(昭和40年)4月14日生まれのいい歳したおっさんでソフトウェア技術者としてはとんでもない業績を打ち立てている物凄い人なんだけどruby会議というrubyの世界的カンファレンスに行く時にわざわざpythonという別のスクリプト言語のロゴが書かれたTシャツを着て行こうとして家族に止められたというどうしようもないエピソードを持っててあと見た目がやくみつるにそっくりでなんか顔写真を見た時に一瞬なんとも言えないドス黒い感情を沸き立たせちゃうような人で、その人が開発したオブジェクト指向スクリプト言語」とかってしたら決してこれはその単語の説明が一貫してまとめられているとは言えません。
これだけmatzの素敵なところを愛情たっぷりに挙げているのに対し、「オブジェクト指向」という言葉については一切触れられていなかったり、同様に「スクリプト言語」というものにも一切説明がないのは不自然さしか感じられません。
これをもっと簡潔に書くとしたらば「まつもとゆきひろによって開発されたオブジェクト指向スクリプト言語」とするほうが適切と言えるでしょう。
この説明文はあくまでrubyについての説明であり、その説明の構成要素の1つであるまつもとゆきひろの子細については、その単語を説明する際に書きだせばいいだけであり、またその説明中に出てくる「島根県」とか「python」とかって単語や「なぜやくみつるを見ただけで妙にイラッとしてしまうのか」ということについての詳細についてはそれぞれの単語の説明で行われる、という入れ子構造になっていればいいはずです。
単語の説明は、その単語の意味を説明できるような別の単語を用いてある順序によって配列させて1つの集合体として扱えるようにすることで成立するのだと思います。
で、結局コードも一種の読み物なわけなのですが、それを複雑なレトリックを用いて書きあげるのではなく、上で言う「単語」と「説明文」をそれぞれ「メソッド名または関数名」と「その処理内容」と置き換えた上で、辞書的に書けばいいのではないのかと考えたのです。
ある名前のつけられたメソッドを、その名前を説明できるような別のメソッドたちをある順序によって配列させて1つの集合体として扱えるようにする、といった風に。
例えば先に出て来たProductモデルで言えば、送料のかかる商品が実は5ドル以下の商品だけでなく、『書籍については10ドル以下の商品が、また衣類については30ドル以下の商品が送料がかかり、それら以外のカテゴリの商品で5ドル以下のものであれば送料がかかる』というものであったとします。
その場合、need_to_pay_postageメソッドはその中に大きくは「10ドル以下の書籍を取得する」メソッドと「30ドル以下の衣類を取得する」メソッドと「書籍と衣類以外のカテゴリの5ドル以下の商品を取得する」メソッドの3つ、つまりは『』で囲まれた「送料がかかる」という言葉の説明に使われる語があればいいんじゃないのという訳です。
「送料がかかる」という名前を説明しようとした時に、その構成要素としてその3つが挙げられます。名前を基準として、それら構成要素の1つ1つを抽象化の単位とすればどれをどこまでまとめよう…と悩むことは無いのでないのかということです。
「名前重要」という考え方が色んな意味合いを持つのだろうとは思いますが、理解しやすいコードを書く上で、「名前重要」がこのような意味合いを持つのではないのか?と考えてみたのです。かなりオレオレ解釈っぽい感じになってしまった思うので俺式としました。
森を森として捉える
この日記を書いているうちに、どういう視点で抽象化するのか・どういう視点でコードをまとめるかというのは書く時に意識すべきことでありながら、また同時にコードを読むときに意識するべきことなんではないのかな、と考えました。
僕が人のコードを読むのに消極的だった理由も、どういう視点で処理を分割してまとめているのかという事が書けない、つまりは理解出来なかったからなんじゃないの?とも思ってます。
わざわざ読みやすいように配慮して書かれているコードであっても、その読みやすくなるポイントが理解出来なければ意味がありません。木を見て森を見ずというか、読み手(僕)の視点が1行1行に対してのモノであるため、処理のブロック単位での意味把握ができないという感じにいつもなっていたような気がします。で、結果として脳内スタックが...とか言いながらうまくコードを読むことが出来ず、コードリーディングに対する苦手意識がついてしまった、みたいな。
絵が悪魔的に下手な人ってたまにいますが、彼らの書き方を見ていると、書こうとしているものの一部に対してこだわり過ぎるというか、その一部しか見ていないために全体的なバランスが物凄く悪くなるという事が多いように感じます。はいだしょうこ画伯の魔獣降臨の儀の動画とか見てると、この人ホワイトボードと顔の距離が異常なくらい近いし、全体が見えてないといういい例なのかもしれません。
逆にうまい人は書こうとしているものの特徴的な部分を捉えて、それを絵の中にいい感じのバランスで組み込めているのが上手さの秘訣なのかなぁとか思ったり。
絵にしてもコードにしても、木を見ずに森を捉えることが重要なんじゃないかな、というわけです。
という感じで長々書いてはみたものの、僕自身、書いたコードは多くないし、自分や他人のコードを保守した経験もありません。なので推測が滅茶苦茶多くなってしまいました。もう少し自信を持った上で論を展開できるようになるためにも、精進したい所存で有ります。相当な数ツッコミどころが有りそうなので、そうじゃねえだろデブ!!みたいなこと思った経験豊富なプログラマーの方々、是非ツッコんでください。よろしくおねがいします。ファミチキとかおごりますんで。どうしてもというなら、Lチキもおごりますんで。