クリアなコードの作り方: オーバースペックな機能を使わない - 2012-11-01 - ククログ

ククログ

株式会社クリアコード > ククログ > クリアなコードの作り方: オーバースペックな機能を使わない

クリアなコードの作り方: オーバースペックな機能を使わない

本当は「…ない」と否定形ではなく「…する」というような肯定形のタイトルにしたかったのですが、すっきりしたタイトルが浮かびませんでした。肯定形で書くと「身の丈にあった機能を使う」です。

このタイトルは、書いている人にとってオーバースペックかどうかではなく、書いているコードにとってオーバースペックかどうかという意味です。初心者だからメタプログラミングはするな、という話ではありません1。やり方をいくつも知っていると、より汎用的なやり方を選択したくなるでしょうが、汎用的かどうかという基準だけで考えるのではなく、そのコードにあったやり方かどうかという基準でも考えましょうという話です。

コードを読む側を経験するとわかりますが、コードを書いた人がどうしてこのようなコードを書いたかを知っているか知っていないかでコードの読みやすさが違います。もちろん、どうして書いたかを知っている方が読みやすいです。例えると、どうしてそう書いたかがわかるときは地図を持って進んでいるような感じで、わからないときは地図なしで進んでいるような感じです。

ここでは、「コードを書いた人がどうしてそう書いたか」ということを「書いた人の意図」と呼ぶことにします。

書いた人の意図はいろんな方法で伝えられます。本人から説明してもらうこともありますし、ドキュメントに書かれていることもあります。ソースコード中のコメントに書かれていることもありますし、テストコードで書かれていたり、コードそれ自身で書かれていることもあります。ここでは、コード自身で意図を伝えるケースだけを考えます。

それでは、(書く人ではなくコードにとって)身の丈にあった機能を使ってコードを書くと、コード自身で意図を伝えやすくなるということを例を使いながら説明します。

例1: ある文字列が先頭にあることをチェックする

1つめの例は、「ある文字列が先頭にあることをチェックするコード」です。コードはPacknga2というRubyのライブラリから持ってきました。

正規表現を使う

まずは、正規表現を使って書いたコードです。

text_files = @spec.files.find_all do |file|
  /\Adoc\/text\// =~ file
end

このコードはファイルの中からテキストファイルだけを抽出するコードです。filesメソッドはファイルのパス(文字列)の配列を返します。このfilesメソッドが返す配列の中から"doc/text/"という文字列で始まっているパスだけを抽出しています。

"doc/text/"という文字列で始まっているかという条件を書くために、上の例では正規表現を使っています。この正規表現は、文字列の先頭という位置を示す\Aという部分と、その後に続くdoc\/text\/という部分からなっています。そのため、この正規表現は「"doc/text/"という文字列が先頭にあるかどうかを判断する」という意味になります。

上の例のように、正規表現を使うと文字列がパターンにマッチするかどうかをチェックできます。正規表現では、今回の例で示したパターン以外もいろいろなパターンを示すことができます。例えば、"doc"という文字列が先頭にあって、末尾に"text/"という文字列がある、といったチェックもできます3。正規表現は短い文字列で様々なパターンを示せる反面、表現の幅が広いためパッと見ただけではどんな文字列にマッチして、どんな文字列にはマッチしないのかを判断しづらいことがあります。

この例では正規表現のたくさんある機能のうち、「\Aを使った文字列の先頭にあるかチェック」と「doc\/text\/を使った"doc/text/"という文字列かチェック」だけを使っています。この機能だけなら正規表現でなくても実現できます。この例に対しては正規表現はオーバースペックではないでしょうか。

ある文字列が先頭にあることをチェックするメソッドを使う

たまたま、Rubyには「ある文字列が先頭にあることをチェックするメソッド」があります。もともとやりたかったこと(「ある文字列が先頭にあることをチェックする」)に対して過不足ない機能です。

text_files = @spec.files.find_all do |file|
  file.start_with?("doc/text/")
end

正規表現ではできることがたくさんあったため、「この機能は使っている」・「この機能は使っていない」ということを注意深く確認する必要がありましたが、String#start_with?を使う場合はレシーバーの文字列が指定した文字列からはじまっているかしかチェックしないため、書いた人の意図が明確になります4

この例からわかること

正規表現など汎用的な機能はできることが多いため便利ですが、やれることが多いため書いた人の意図が伝わりにくくなる可能性もあります。一方、start_with?のように小さな機能は、やれることが限定されるため、書いた人の意図が伝わりやすくなります。もし、小さな機能で十分であればそちらを優先して使うと、書いた人の意図が伝わりやすいコードになるでしょう。

例2: コメントアウト

次の例はコメントアウトする例です。

以下のような文字列があったとします。

def comment_out(string)
  # ...
end

この文字列に対して以下のように「すべての先頭行に#を追加」したいということです。

# def comment_out(string)
#   # ...
# end

正規表現を使う

正規表現を使うと以下のように書けます。

code = <<-EOC
def comment_out(string)
  # ...
end
EOC

code.gsub(/^/, "# ")

この正規表現は^しかないので何にマッチする正規表現なのかすぐにわかります。そのため、正規表現といういろんなことができる機能を使っていても意図が伝わりにくくなるという可能性は低いです。

すべての行に処理をするメソッドを使う

それでは、正規表現ではなくすべての行に処理をするメソッドを使ってみましょう。「すべての行の先頭に指定した文字列を追加する」メソッドがあるなら、そのメソッドがそのものズバリですが、このメソッドは少し機能が足りません。「すべての行に処理をする」ことができるだけで「行頭に指定した文字列を追加する」ことはできません。

code = <<-EOC
def comment_out(string)
  # ...
end
EOC

commented_code = ""
code.each_line do |line|
  commented_code << "# #{line}"
end
commented_code

各行の先頭に"# "を追加するコードです。実現したいことを素直に書いているのでわかりにくいということはありませんが、正規表現の実装を見たあとでは冗長な感じがします。そのものズバリではない機能を使う場合は書いた人の意図が少し伝わりにくくなります。

この例からわかること

やれることが多いため書いた人の意図が伝わりにくいことがある正規表現ですが、この例では伝わりやすく書けています。一方、each_lineという小さな機能を使った例の方はパッとみたときの意図が伝わりにくくなっています。

ということで、正規表現だから必ずしも意図が伝わりにくくなるということはありません。

なお、以下のようにメソッドにすれば、どちらの書き方でもより意図が伝わりやすくなります。

def comment_out(code)
  code.gsub(/^/, "# ")
end

def comment_out(code)
  commented_code = ""
  code.each_line do |line|
    commented_code << "# #{line}"
  end
  commented_code
end

メソッドにするということは、コメントアウトしかしない小さな機能を作ったということです。以下のコードの方が正規表現を使った場合やeach_lineを使った場合よりも書いた人の意図が伝わります。コードを書いた人はコメントアウトしたかったんでしょう。

comment_out(code)

まとめ

正規表現のようにできることが多い汎用的な機能は便利ですが、書いた人の意図が伝わりにくくなる可能性があるので注意してください5。汎用的な機能はそれを使いこなせれば細々とした機能の使い方を覚える必要がなくなるので、書く人の学習コストを抑えられる場合があります6。汎用的な機能で書いたコードが自分の意図を伝えているかを確認してみてください。もし、やりたいことを実現する小さな機能があった場合は、汎用的な機能ではなく、小さな機能を使ったほうが書いた人の意図が伝わりやすくなります。オーバースペックな機能よりも身の丈にあった機能を使いましょう。

もし、身の丈にあった機能がなかった場合は自分で作ることもできます。書いたコードを読んで、自分の意図が伝わっているか考えてみてはいかがでしょうか。

  1. 念のため書いておくと、初心者でもメタプログラミングしていいよ、と言っているわけではなくて、どうしろとかいい悪いとかは何も言っていないということです。

  2. Packngaは多言語に対応したドキュメントを生成するためのユーティリティを集めたライブラリです。

  3. このチェックをするための正規表現は/\Adoc.*text\/\z/となります。

  4. ただし、start_with?がどのようなメソッドかがわからないとパッと見ただけではわかりにくいかもしれません。メソッド名から類推できるような気はしますが。。。

  5. ただし、いつも意図が伝わりにくくなるわけではないので、「正規表現禁止!」となるのは考えものです。

  6. 汎用的な機能は覚えることが大変なので、もしかしたら、それほど大差はないかもしれません。