最近読んだRubyのコードではYARDのコードがキレイでした。
さて、長いメソッドは不吉なにおいがするからメソッドを分割するなどして短くしましょうとはよく言われることですが、ここでいう「長い」とは「縦に長い」ことを指していることがほとんどです。長いのが問題なのは縦に長いときだけではなく横に長いときもです。
まず、どうして縦に長いメソッドが問題かについてです。縦に長いメソッドには「処理を把握しづらい」という問題がある可能性が高いです。
処理を把握しづらい原因はいくつかあります。例えば、抽象度が低いのが原因です。
メソッドが縦に長くなっているときは、多くの処理が行われていることがほとんどです。これらの処理はメソッドになっていないため名前がついていません。処理に名前がついていない場合は実装を読まないとなにをしているかがわかりません。
せっかくなので実例を元にしてメソッドが長いのがどうして問題なのか、また、どのようによくすればよいかを説明します。例にするのはlogaling-commandのLogaling::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_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 |
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_name
とalice.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_name
やsource_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/2/13にEmacsでlogaling-commandを利用するためのフロントエンドlogalimacsをリリースしました。
logaling-commandは翻訳作業に欠かせない訳語の確認や選定をサポートする CUI ツールです。 「対訳用語集」を簡単に作成、編集、検索することができます。
logalimacsはEmacsからlogalingを利用するためのフロントエンドです。 CUIで対訳用語集を利用するよりもエディタ上でシームレスに対訳用語集を利用できるとより翻訳作業が捗るため、開発に着手しました。
Emacsを使っていて何らかのドキュメントの翻訳中に英単語を調べる時に、わざわざブラウザに切り替えたくないですよね?
そこでlogalimacsの出番です。C-:
を押すと、
カーソル位置の単語で対訳用語集を検索します。もしカーソル位置が空白の場合はそれより前の単語で対訳用語集を検索します。このコマンドはpopupで検索結果を表示します
*1。
また任意の情報から調べたい場合C-u
を押してからC-:
を押して下さい。
minibuffer に入力ボックスが出るので、そこに入力した単語で検索できます。
もしリージョンで選択した単語も検索したい場合は、リージョンの文字列が優先して検索されます。
簡単なインストール方法は以下の通りです。 logalimacsを使いたくてウズウズしている方は試してみてください*2。
詳しいインストール方法の説明はlogalimacsのチュートリアルページで紹介しています。
% gem install logaling-command
辞書のインポートは1分から2分かかります。
% loga import gene95 % loga import edict
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を紹介しました。ぜひ使ってみてください!
「分かりやすいコードを書く」、「コードと一緒にテストも書く」等はソフトウェア開発において大切なことです。しかしそれと同じくらい大切なことして「分かりやすいコミットメッセージを書く」があります。これはあまり着目されていなく、見過ごされていることです。
今回は、コミットメッセージの分かりやすさの大切さ、そして、分かりやすくするための書き方を説明します。
現在、ほとんど全てのソフトウェア開発ではSubversionやGitなどのバージョン管理システムを使っています。バージョン管理システムを使うことによるメリットというのは、ソフトウェアの変更が記録されていくことにあります。
具体的なメリットは3つあります。
このようなメリットからバージョン管理システムはソフトウェア開発に広く使われているわけです。
バージョン管理システムを前提とした上で、ソフトウェア開発を説明するならば、「バージョン管理システムに記録される変更を継続的に作成し、積み重ねていく作業」と言えます。この記録される変更というのは、具体的にはソフトウェアの変更ということになります。今回の記事では、この変更されるものを「コード」と呼ぶことにします。
以上の説明を元にして用語を整理します。バージョン管理システムでは個々に記録されていくコードの変更を「コミット」と呼びます。コミットされたコードの変更を説明している文章を「コミットメッセージ」と呼びます。
「コミット」の分かりやすさの大切さを説明する前に、まずは、「コード」の分かりやすさの大切さを説明します。
なぜコードの分かりやすさは大切なのでしょうか?理由は、コードはソフトウェア開発を通して何度も読まれるからです。多くの人に読まれる本や雑誌の文章は分かりやすいことが大切であるように、何度も読まれるコードも分かりやすいことが大切です。
では次にコミットの分かりやすさの大切さについてです。上で説明したバージョン管理システムの具体的なメリットからも分かるように、コードとコミットは常に一緒に読まれます。なので、一緒に読まれるコードと同じように、コミットも分かりやすいことが大切なのです。どんなに分かりやすいコードでもコミットが分かりにくいならば片手落ちになります。言い方を変えれば、コードと同じくらいに、コミットはソフトウェア開発の作業の重要な成果物となります。
では、コミットの分かりやすさとは何でしょうか?分かりやすいコミットとは、次の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)
一般的に、統一感があると分かりやすくなります。なのでコードのスタイルが統一されていると分かりやすいのと同様に、コミットメッセージのスタイルも統一されていると分かりやすいです。
具体的には、時制を過去にするか現在にするか、ピリオド(句読点)を含めるかどうか、大文字や小文字(全角や半角)の使い方、文章形式にするか名詞形式にするかなど、様々な基準があります。
コーディングスタイルがそうであるように、コミットメッセージのスタイルは、ソフトウェア開発全体では統一されていません。だからといって各人がバラバラのスタイルで書くよりは、開発しているソフトウェア単位でスタイルを決め、統一すると分かりやすくなります。
悪い例:
良い例:
ちなみに、今回の記事では次のスタイルを採用しています(Gitスタイル)。
主要なプログラミング言語は、英語が前提となっており、コードも同様です。上で説明した統一感のためにも、コミットも英語で書くと分かりやすいです。
ソフトウェア開発では英語の読み書きが必要となります。日頃から英語で書くことで、英語に慣れることができるというメリットがあります。
また、英語は世界でもっとも広く使われている言語であり、多くの人が読めます。つまり、多くの人が開発に参加できるフリーソフトウェア開発の場合は、英語を使うことはことさら大切になります。
悪い例:
新しいデータ型に対応
良い例:
Support new data types
今回はコミットメッセージについて、分かりやすいことは大切であり、分かりやすくするための書き方を説明しました。
具体的な書き方として、長すぎず短すぎず、統一されたスタイルで、英語のコミットメッセージを書けば分かりやすくなるということを説明しました。
4年に1度のうるう肉の日ということもあり、groongaとmroongaがメジャーバージョンアップしてリリースされました。
groonga, mroongaは毎月定期的にリリースされており、このリリースで劇的に変化したわけではありません。しかし、1.0.0がリリースされた時点からは劇的に改良されています。1.0.0の頃からしばらく忘れていたなぁという方にぜひ確認してもらいたいリリースです。
バージョンだけではなく見た目も変えてアピールしよう!ということで、メジャーバージョンアップにあわせてロゴも更新しました。
前のロゴを作ったときは、たくさんのgroonga関連のプロジェクトがXroongaという名前になることを予想していませんでした。前のロゴはお面をモチーフにしており、関連プロジェクトで同じようなロゴを作りづらいという問題がありました。
そこで、今回のロゴは最初の1文字をカスタマイズしやすいデザインになっています。実は、"a"から"z"までの文字を別途用意してあるので、「proonga」など新しくXroongaな名前のプロジェクトを作ったときも再利用しやすくなっています。
ロゴは誰でも自由に利用できるように準備を進めているので、groonga関連のプロジェクトに関わっている方は期待してもう少しお待ちください!
リリースに関する情報はgroongaやmroongaのサイトで紹介しているので、ここでは、新しいロゴについて紹介しました。サイトデザインの更新も進めているので、そちらも楽しみにしていてください。
また、groongaを使った検索システムの検討から運用まで支援するサービスもはじめました。興味のある方はお気軽にお問い合わせください。
(プログラミングが好きでgroonga関連の開発に興味のある方は採用情報をご覧ください。)