株式会社クリアコード > ククログ

ククログ


クリアなコードの作り方: 縦長・横長なメソッドを短くする

最近読んだRubyのコードではYARDのコードがキレイでした。

さて、長いメソッドは不吉なにおいがするからメソッドを分割するなどして短くしましょうとはよく言われることですが、ここでいう「長い」とは「縦に長い」ことを指していることがほとんどです。長いのが問題なのは縦に長いときだけではなく横に長いときもです。

縦に長いメソッド

まず、どうして縦に長いメソッドが問題かについてです。縦に長いメソッドには「処理を把握しづらい」という問題がある可能性が高いです。

どうして処理を把握しづらいか

処理を把握しづらい原因はいくつかあります。例えば、抽象度が低いのが原因です。

メソッドが縦に長くなっているときは、多くの処理が行われていることがほとんどです。これらの処理はメソッドになっていないため名前がついていません。処理に名前がついていない場合は実装を読まないとなにをしているかがわかりません。

せっかくなので実例を元にしてメソッドが長いのがどうして問題なのか、また、どのようによくすればよいかを説明します。例にするのはlogaling-commandLogaling::Command::Application#lookupです。これを例に選んだ理由は、開発チームも整理したほうがよいコードだと認識しているコードだからです*1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def lookup(source_term)
  config = load_config_and_merge_options
  repository.index
  terms = repository.lookup(source_term, config["source-language"], config["target-language"], config["glossary"])

  unless terms.empty?
    max_str_size = terms.map{|term| term[:source_term].size}.sort.last
    run_pager
    puts("[") if "json" == options["output"]
    terms.each_with_index do |term, i|
      target_string = "#{term[:target_term].bright}"
      target_string <<  "\t# #{term[:note]}" unless term[:note].empty?
      if repository.glossary_counts > 1
        target_string << "\t"
        glossary_name = "(#{term[:glossary_name]})"
        if term[:glossary_name] == config["glossary"]
          target_string << glossary_name.foreground(:white).background(:green)
        else
          target_string << glossary_name
        end
      end
      source_string = term[:snipped_source_term].map{|word|  word.is_a?(Hash) ? word[:keyword].bright : word }.join
      source, target = source_string, target_string.split("\t").first
      note = term[:note]
      source_language, target_language = config["source-language"], config["target-language"]
      case options["output"]
      when "terminal"
        printf("  %-#{max_str_size+10}s %s\n", source_string, target_string)
      when "csv"
        print(CSV.generate {|csv| csv << [source_string, target, note, source_language, target_language]})
      when "json"
        puts(",") if i > 0
        record = {
          :source => source_string, :target => target,
          :note => note,
          :source_language => source_language, :target_language => target_language
        }
        print JSON.pretty_generate(record)
      end
    end
    puts("\n]") if "json" == options["output"]
  else
    "source-term <#{source_term}> not found"
  end
rescue Logaling::CommandFailed, Logaling::TermError => e
  say e.message
end

今回のサンプルの中には以下の一行があります。

1
terms = repository.lookup(source_term, config["source-language"], config["target-language"], config["glossary"])

このコードからは「ソース(翻訳元)の単語(source_term)とソースの言語(config["source-language"])とターゲット(翻訳先)の言語(config["target-language"])と用語集(config["glossary"])を使ってリポジトリ(repository)から単語(terms)を検索している(lookup)」ことがわかります。しかし、具体的にどのように検索しているかはわかりません。しかしそれでよいのです。コードを読むときは「ここで検索して単語を取得できた」という前提で読み進めます。こうすることで考えることを少なくできて、問題に集中できます。これが抽象化されているということです。

一方、以下の部分は何をしているかを知るために具体的に何をしているかを読まないといけません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
unless terms.empty?
  max_str_size = terms.map{|term| term[:source_term].size}.sort.last
  run_pager
  puts("[") if "json" == options["output"]
  terms.each_with_index do |term, i|
    target_string = "#{term[:target_term].bright}"
    target_string <<  "\t# #{term[:note]}" unless term[:note].empty?
    if repository.glossary_counts > 1
      target_string << "\t"
      glossary_name = "(#{term[:glossary_name]})"
      if term[:glossary_name] == config["glossary"]
        target_string << glossary_name.foreground(:white).background(:green)
      else
        target_string << glossary_name
      end
    end
    source_string = term[:snipped_source_term].map{|word|  word.is_a?(Hash) ? word[:keyword].bright : word }.join
    source, target = source_string, target_string.split("\t").first
    note = term[:note]
    source_language, target_language = config["source-language"], config["target-language"]
    case options["output"]
    when "terminal"
      printf("  %-#{max_str_size+10}s %s\n", source_string, target_string)
    when "csv"
      print(CSV.generate {|csv| csv << [source_string, target, note, source_language, target_language]})
    when "json"
      puts(",") if i > 0
      record = {
        :source => source_string, :target => target,
        :note => note,
        :source_language => source_language, :target_language => target_language
      }
      print JSON.pretty_generate(record)
    end
  end
  puts("\n]") if "json" == options["output"]
else
  "source-term <#{source_term}> not found"
end

これが抽象度が低いということです。抽象度が低い部分は細かく実装を読まないといけないため抽象度が高い部分に比べて処理を把握しづらくなります。なお、この部分は見つけた単語を整形して表示している部分です。

キレイにする方法

縦に長い場合はメソッドを分割します。これはよく言われていることですね。

今回のサンプルでは以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
def lookup(source_term)
  config = load_config_and_merge_options
  repository.index
  terms = repository.lookup(source_term, config["source-language"], config["target-language"], config["glossary"])

  if terms.empty?
    "source-term <#{source_term}> not found"
  else
    report_terms(terms, config)
  end
rescue Logaling::CommandFailed, Logaling::TermError => e
  say e.message
end

これならLogaling::Command::Application#lookupが何をしているのかはすぐにわかりますね。「単語を検索して見つかった単語を出力」しています。

report_termslookupにあったコードそのままです。そのままですが、「このメソッドは単語を出力するメソッド」と思って読むと、そうと知らずに読むときよりも理解しやすくなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private
def report_terms(terms, config)
  max_str_size = terms.map{|term| term[:source_term].size}.sort.last
  run_pager
  puts("[") if "json" == options["output"]
  terms.each_with_index do |term, i|
    target_string = "#{term[:target_term].bright}"
    target_string <<  "\t# #{term[:note]}" unless term[:note].empty?
    if repository.glossary_counts > 1
      target_string << "\t"
      glossary_name = "(#{term[:glossary_name]})"
      if term[:glossary_name] == config["glossary"]
        target_string << glossary_name.foreground(:white).background(:green)
      else
        target_string << glossary_name
      end
    end
    source_string = term[:snipped_source_term].map{|word|  word.is_a?(Hash) ? word[:keyword].bright : word }.join
    source, target = source_string, target_string.split("\t").first
    note = term[:note]
    source_language, target_language = config["source-language"], config["target-language"]
    case options["output"]
    when "terminal"
      printf("  %-#{max_str_size+10}s %s\n", source_string, target_string)
    when "csv"
      print(CSV.generate {|csv| csv << [source_string, target, note, source_language, target_language]})
    when "json"
      puts(",") if i > 0
      record = {
        :source => source_string, :target => target,
        :note => note,
        :source_language => source_language, :target_language => target_language
      }
      print JSON.pretty_generate(record)
    end
  end
  puts("\n]") if "json" == options["output"]
end

なお、report_terms内ではターミナル出力・CSV出力・JSON出力の3種類の出力するためのコードが入っています。そのため、report_termsをさらに短くする場合はそこに注目してメソッドを分割することになります。

横に長いメソッド

それでは、次に、どうして横に長いメソッドが問題かについてです。横に長いメソッドには「遠くのオブジェクトにさわっている」という問題がある可能性が高いです。

どうして遠くのオブジェクトにさわるのが問題か

まず、遠くのオブジェクトにさわるということを説明します。

プログラムは「ここではこういう状態でプログラムが動く」という前提を踏まえながら書きます。例えば、メソッドの中で「このインスタンス変数」といえば「自分のインスタンス変数」という前提で書きます。

1
2
3
4
5
6
7
8
9
10
11
class Person
  attr_reader :first_name, :last_name
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def full_name
    "#{@first_name} #{@last_name}"
  end
end

このような前提がコンテキストです。同じコンテキストでは短い記述で書くことができ、違うコンテキストでは記述が長くなります。

1
2
alice = Person.new("Alice", "Liddell")
puts "#{alice.first_name} #{alice.last_name}"

メソッドの中では@first_name@last_nameでアクセスできたものがトップレベルではalice.first_namealice.last_nameでアクセスすることになります。これはコンテキストが異なるため、単に「インスタンス変数」ということができずに「aliceのインスタンス変数」という必要があるためです。

遠くのオブジェクトというのは離れたコンテキストにあるオブジェクトのことです。遠くのオブジェクトにアクセスするには以下のようにコンテキストをたどっていく必要があります。

1
bookstore.fairy_stories.find {|story| story.title == "Alice's Adventures in Wonderland"}.characters.find {|character| character.first_name == "Alice"}.full_name

コンテキストをたどっていくとコードが横に長くなります。

それでは、どうして遠くのオブジェクトにさわるのが問題なのでしょうか。それは、抽象度が低くなるからです。また抽象度です。縦に長いメソッドのところでも触れましたが、抽象度が低いと多くのことを把握しないといけなくなります。しかし、多くのことを把握することは大変です。大きなソフトウェアや久しぶりにさわるソフトウェアでは特に大変です。そのため、できるだけ必要なことだけを把握した状態でプログラムを書けるようにしたいのです。遠くのオブジェクトにさわるコードではそれが難しくなることが問題です。

サンプル

logaling-commandや近くにあったtDiaryを見てみましたが、あまりこのケースに該当するコードはありませんでした。しかし、無理やり引っ張りだしてきたのが以下のコードです。これは用語を検索するために用語集のインデックスを更新するLogaling::Repository#indexというメソッドです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def index
  project_glossaries = Dir[File.join(@path, "projects", "*")].map do |project|
    Dir.glob(get_all_glossary_sources(File.join(project, "glossary")))
  end
  imported_glossaries = Dir.glob(get_all_glossary_sources(cache_path))
  all_glossaries = project_glossaries.flatten + imported_glossaries

  Logaling::GlossaryDB.open(logaling_db_home, "utf8") do |db|
    db.recreate_table
    all_glossaries.each do |glossary_source|
      indexed_at = File.mtime(glossary_source)
      unless db.glossary_source_exist?(glossary_source, indexed_at)
        glossary_name, source_language, target_language = get_glossary(glossary_source)
        puts "now index #{glossary_name}..."
        db.index_glossary(Glossary.load(glossary_source), glossary_name, glossary_source, source_language, target_language, indexed_at)
      end
    end
    (db.get_all_glossary_source - all_glossaries).each do |glossary_source|
      glossary_name, source_language, target_language = get_glossary(glossary_source)
      puts "now deindex #{glossary_name}..."
      db.deindex_glossary(glossary_name, glossary_source)
    end
  end
end

気になるのはこのあたりです。

1
2
3
4
5
6
7
8
all_glossaries.each do |glossary_source|
  indexed_at = File.mtime(glossary_source)
  unless db.glossary_source_exist?(glossary_source, indexed_at)
    glossary_name, source_language, target_language = get_glossary(glossary_source)
    puts "now index #{glossary_name}..."
    db.index_glossary(Glossary.load(glossary_source), glossary_name, glossary_source, source_language, target_language, indexed_at)
  end
end

glossary_namesource_languageなどをバラバラにdb.index_glossaryに渡さないでGlossaryオブジェクトを渡すというのはどうでしょうか。

1
2
3
4
5
6
7
8
all_glossaries.each do |glossary_source|
  Glossary.open(glossary_source) do |glossary|
    unless db.glossary_source_exist?(glossary)
      puts "now index #{glossary.name}..."
      db.index_glossary(glossary)
    end
  end
end

だいぶすっきりしました。これでこのメソッドはGlossaryがどのような情報を持っているかの詳細を知らずに済みます。単に「用語集として必要な情報を持っているはず」ということだけ把握していればよいのです。

まとめ

メソッドの長さは視覚的にわかるので、パッと見てキレイなコードかそうでないかをざっくりと判断しやすくて便利です。あなたのコードはパッと見てキレイですか?

*1 すでに修正済みです。

2012-02-07

logalimacsをリリースしました

2012/2/13にEmacsでlogaling-commandを利用するためのフロントエンドlogalimacsをリリースしました。

logaling-commandとは

logaling-commandは翻訳作業に欠かせない訳語の確認や選定をサポートする CUI ツールです。 「対訳用語集」を簡単に作成、編集、検索することができます。

logalimacsとは

logalimacsはEmacsからlogalingを利用するためのフロントエンドです。 CUIで対訳用語集を利用するよりもエディタ上でシームレスに対訳用語集を利用できるとより翻訳作業が捗るため、開発に着手しました。

使い方

Emacsを使っていて何らかのドキュメントの翻訳中に英単語を調べる時に、わざわざブラウザに切り替えたくないですよね? そこでlogalimacsの出番です。C-:を押すと、 カーソル位置の単語で対訳用語集を検索します。もしカーソル位置が空白の場合はそれより前の単語で対訳用語集を検索します。このコマンドはpopupで検索結果を表示します *1

Emacsでpopupしたところ

また任意の情報から調べたい場合C-uを押してからC-:を押して下さい。 minibuffer に入力ボックスが出るので、そこに入力した単語で検索できます。

もしリージョンで選択した単語も検索したい場合は、リージョンの文字列が優先して検索されます。

インストール

簡単なインストール方法は以下の通りです。 logalimacsを使いたくてウズウズしている方は試してみてください*2

詳しいインストール方法の説明はlogalimacsのチュートリアルページで紹介しています。

logaling-commandのインストール
% gem install logaling-command
辞書のインポート

辞書のインポートは1分から2分かかります。

% loga import gene95
% loga import edict
logalimacsのインストール

GitHubからlogalimacsをcloneします。

% cd ~/.emacs.d/
% git clone git://github.com/logaling/logalimacs.git

~/.emacs.d/init.elに以下を追加します。

(add-to-list 'load-path "~/.emacs.d/logalimacs")
(autoload 'loga-lookup-in-popup "logalimacs" nil t)
(global-set-key (kbd "C-:") 'loga-lookup-in-popup)

これでlogalimacsを利用するための設定は終わりです。 試しに、*scratch*バッファに「ruby」と入力してC-:を押してみてください。 上記のスクリーンショットのような訳がでるはずです*3

まとめ

logaling-commandをEmacsから簡単につかうためのlogalimacsを紹介しました。ぜひ使ってみてください!

*1 popup.elを利用しています。

*2  Emacs24を利用されている方は、Marmalade経由でインストールできます。そのためEmacs側の設定は、global-set-keyの部分だけになります。

*3  プログラミング言語Rubyという部分は個別に登録したものなので表示されません。

2012-02-13

コミットメッセージの書き方

はじめに

「分かりやすいコードを書く」、「コードと一緒にテストも書く」等はソフトウェア開発において大切なことです。しかしそれと同じくらい大切なことして「分かりやすいコミットメッセージを書く」があります。これはあまり着目されていなく、見過ごされていることです。

今回は、コミットメッセージの分かりやすさの大切さ、そして、分かりやすくするための書き方を説明します。

コミットメッセージとその大切さ

バージョン管理システムとコミット

現在、ほとんど全てのソフトウェア開発ではSubversionやGitなどのバージョン管理システムを使っています。バージョン管理システムを使うことによるメリットというのは、ソフトウェアの変更が記録されていくことにあります。

具体的なメリットは3つあります。

  1. ソフトウェアの調査がしやすくなることです。現時点でのコードと、そして変更の履歴とを組み合わせることで、それらから非常に多くの情報を得ることができます。
  2. ソフトウェアを開発している人からレビューを受けられることです。自分の変更をチェックしてもらい、改善点を含めさまざまなアドバイスを貰うことができます。
  3. ソフトウェアの変更を元に戻せることです。各変更がそれぞれ独立して記録されているので、時間が経過し他の追加の変更がある状態でも、過去の特定の変更だけを取り消すことができます。

このようなメリットからバージョン管理システムはソフトウェア開発に広く使われているわけです。

バージョン管理システムを前提とした上で、ソフトウェア開発を説明するならば、「バージョン管理システムに記録される変更を継続的に作成し、積み重ねていく作業」と言えます。この記録される変更というのは、具体的にはソフトウェアの変更ということになります。今回の記事では、この変更されるものを「コード」と呼ぶことにします。

以上の説明を元にして用語を整理します。バージョン管理システムでは個々に記録されていくコードの変更を「コミット」と呼びます。コミットされたコードの変更を説明している文章を「コミットメッセージ」と呼びます。

コミットの分かりやすさ

「コミット」の分かりやすさの大切さを説明する前に、まずは、「コード」の分かりやすさの大切さを説明します。

なぜコードの分かりやすさは大切なのでしょうか?理由は、コードはソフトウェア開発を通して何度も読まれるからです。多くの人に読まれる本や雑誌の文章は分かりやすいことが大切であるように、何度も読まれるコードも分かりやすいことが大切です。

では次にコミットの分かりやすさの大切さについてです。上で説明したバージョン管理システムの具体的なメリットからも分かるように、コードとコミットは常に一緒に読まれます。なので、一緒に読まれるコードと同じように、コミットも分かりやすいことが大切なのです。どんなに分かりやすいコードでもコミットが分かりにくいならば片手落ちになります。言い方を変えれば、コードと同じくらいに、コミットはソフトウェア開発の作業の重要な成果物となります。

では、コミットの分かりやすさとは何でしょうか?分かりやすいコミットとは、次の2つ条件に当てはまるものです。

  1. コミットの内容が分かりやすく説明されていること
  2. コミットの内容が小さくまとまっていること

今回は1つ目の条件を説明します。

コミットの内容が分かりやすいためには、具体的にはコミットメッセージが分かりやすいことが大切です。なぜならばコミットの内容を説明するために、コミットメッセージがあるからです。

ではコミットメッセージを分かりやすくするためには、どうすればいいのでしょうか?

長すぎず短すぎず

コミットメッセージは長すぎても短すぎても分かりにくくなります。長さの明確な基準はありませんが、ちょうどいい長さのコミットメッセージの目安として、コードを見なくともコミットメッセージだけから、そのコミットで行われているコードの変更がうまく想像できるかどうかです。

長すぎる場合

一度に多くの情報が含まれていると内容を把握しきれず、分かりにくくなります。コミットを詳しく説明するのは大切ですが、まずは簡単に説明し、次に詳しく説明すると分かりやすくなります。

悪い例:

repository: Ensure that the path to the .git directory ends with a forward slash when opening a repository through a working directory path

良い例:

repository: Fix bug in opening a repository in a certain case

There is a bug in opening a repository through a working
directory path.

Fix it by ensuring that the path to the .git directory ends
with a forward slash.
短すぎる場合

コミットメッセージで具体的にどんな変更をしているのかという情報が少なすぎると分かりにくくなります。実際にコードを見てから、初めて意味が理解できるコミットメッセージなどがその例です。コミットメッセージだけでコードの変更が想像できる位の情報を説明すると分かりやすくなります。

悪い例 (1):

Ate a letter

良い例 (1):

Fix a typo of a missing letter of variable name by adding it

悪い例 (2):

In-progress.

良い例 (2):

Work on new GC methods (in progress)

統一されたスタイル(文体)で書く

一般的に、統一感があると分かりやすくなります。なのでコードのスタイルが統一されていると分かりやすいのと同様に、コミットメッセージのスタイルも統一されていると分かりやすいです。

具体的には、時制を過去にするか現在にするか、ピリオド(句読点)を含めるかどうか、大文字や小文字(全角や半角)の使い方、文章形式にするか名詞形式にするかなど、様々な基準があります。

コーディングスタイルがそうであるように、コミットメッセージのスタイルは、ソフトウェア開発全体では統一されていません。だからといって各人がバラバラのスタイルで書くよりは、開発しているソフトウェア単位でスタイルを決め、統一すると分かりやすくなります。

悪い例:

  • Fixed a bug
  • Adds A Test
  • implement nested comments
  • I cleaned the parser code up.
  • performance improvement

良い例:

  • Fix a bug
  • Add a test
  • Implement nested comments
  • Clean the parser code up
  • Improve performance

ちなみに、今回の記事では次のスタイルを採用しています(Gitスタイル)。

  • 言語は英語にする
  • 一文の場合にはピリオドを付けない
  • 主語は省き時制は現在の文章形式にする
  • 文頭の英単語を大文字にする

英語で書く

主要なプログラミング言語は、英語が前提となっており、コードも同様です。上で説明した統一感のためにも、コミットも英語で書くと分かりやすいです。

ソフトウェア開発では英語の読み書きが必要となります。日頃から英語で書くことで、英語に慣れることができるというメリットがあります。

また、英語は世界でもっとも広く使われている言語であり、多くの人が読めます。つまり、多くの人が開発に参加できるフリーソフトウェア開発の場合は、英語を使うことはことさら大切になります。

悪い例:

新しいデータ型に対応

良い例:

Support new data types

まとめ

今回はコミットメッセージについて、分かりやすいことは大切であり、分かりやすくするための書き方を説明しました。

具体的な書き方として、長すぎず短すぎず、統一されたスタイルで、英語のコミットメッセージを書けば分かりやすくなるということを説明しました。

2012-02-21

groonga 2.0.0, mroonga 2.00リリース

4年に1度のうるう肉の日ということもあり、groongaとmroongaがメジャーバージョンアップしてリリースされました。

groonga, mroongaは毎月定期的にリリースされており、このリリースで劇的に変化したわけではありません。しかし、1.0.0がリリースされた時点からは劇的に改良されています。1.0.0の頃からしばらく忘れていたなぁという方にぜひ確認してもらいたいリリースです。

ロゴの更新

バージョンだけではなく見た目も変えてアピールしよう!ということで、メジャーバージョンアップにあわせてロゴも更新しました。

準備中のロゴ

前のロゴを作ったときは、たくさんのgroonga関連のプロジェクトがXroongaという名前になることを予想していませんでした。前のロゴはお面をモチーフにしており、関連プロジェクトで同じようなロゴを作りづらいという問題がありました。

そこで、今回のロゴは最初の1文字をカスタマイズしやすいデザインになっています。実は、"a"から"z"までの文字を別途用意してあるので、「proonga」など新しくXroongaな名前のプロジェクトを作ったときも再利用しやすくなっています。

ロゴは誰でも自由に利用できるように準備を進めているので、groonga関連のプロジェクトに関わっている方は期待してもう少しお待ちください!

まとめ

リリースに関する情報はgroongaやmroongaのサイトで紹介しているので、ここでは、新しいロゴについて紹介しました。サイトデザインの更新も進めているので、そちらも楽しみにしていてください。

また、groongaを使った検索システムの検討から運用まで支援するサービスもはじめました。興味のある方はお気軽にお問い合わせください。

(プログラミングが好きでgroonga関連の開発に興味のある方は採用情報をご覧ください。)

タグ: Groonga
2012-02-29

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|