はじめに
僕が主催している西脇.rbの勉強会では、毎回「参加者が自分でコードを書く」「参加者がお互いにコードレビューする」ということを重視しています。
一言で言うと、「自分の手と頭を動かす勉強会」になっています。
先日開催した「Rubyプログラミング Dojo」でも、お題となるプログラミング問題を各自が解いて、最後に全員でコードレビューしました。
Photo by: @spring_aki
その勉強会の参加者に岡田さん(@shinokada)という方がいます。
岡田さんは勉強会が終わったあとも自分でプログラミング問題を見つけて、解答となるRubyのコードを書いてきてくれました。
ただし、岡田さんはまだRubyを始めてそれほど長くないので、そのコードには改善する余地がまだまだあります。
そこで、岡田さんが書いてきてくれたそのコードを僕の方でレビューし、いろいろとリファクタリングしてみました。
さらに、その一部始終をスクリーンキャストとして録画してみました。
スクリーンキャストを作成するのは今回初めてだったので、それはそれで僕の動画作成スキルにも改善の余地がたくさんあります。
しかし、動画自体にはRuby初心者の参考になりそうなリファクタリングの手法や考え方がたくさん詰まっていると思います。
今回のエントリではこの動画の内容を紹介してみます。
スクリーンキャストについて
僕が作成したスクリーンキャストはこちらになります。
スクリーンキャストで学ぶRubyリファクタリング: Keitai Message編 - YouTube
画質を上げないと文字が潰れてしまうと思うので、プレーヤーの歯車マークから画質を上げてから視聴してください。
720p(HD)以上(それとPC全画面表示)であれば文字が読めると思います。
再生時間はなんと1時間半もあります!
短いプログラムだし最初はもっと早く終わるだろうと思っていたのですが、思いのほか時間がかかってしまいました。
編集は全くしていないので、僕が「うーん、うーん」と試行錯誤するところも含めて全部ノーカットで収録しています。
ただ、このままだと長すぎて確実に視聴者全員が途中離脱すると思うので、このあとで見どころとなるポイントを紹介しようと思います。
2014.04.26 追記: 再生スピードを上げることもできます
さっき知ったんですが、youTubeには再生スピードを変更する機能があるんですね。
スピードを上げた方が効率よく視聴できそうです。
スクリーンキャストのお題は「ケータイ文字入力」
今回、岡田さんが選んできた問題はこちらの「Keitai Message」というプログラミング問題です。
Keitai Message - AIZU ONLINE JUDGE
この問題は簡単に言うと、テンキーを使ったガラケーの英語文字入力です。
つまり、「2」キーを2回押すと「b」になり、「3」キーを3回押すと「f」になります。
同じキーを何度も押すと、「abcabcabc...」と文字がループします。
たとえば、「2」キーを4回押したときは「a」になります。
普通の携帯電話と違うのは「0」で文字を確定させる点です。
「0」は連続して押すことができますが、空打ちしても何も出力されません。
テンキーと文字の割り当ては以下のようになっています。
1: . , ! ? (スペース) 2: a b c 3: d e f 4: g h i 5: j k l 6: m n o 7: p q r s 8: t u v 9: w x y z 0: 確定ボタン
インプットとアウトプットはそれぞれ文字列として表現します。
実行例を以下に示します。
INPUT: 5 20 220 222220 44033055505550666011011111090666077705550301110 000555555550000330000444000080000200004440000 OUTPUT: (最初の5は「0」で確定していないので何も出ない) a b b hello, world! keitai
岡田さんが書いたリファクタリング前のコード
最初、岡田さんが書いてきてくれたコードはこんな感じでした。
class Keitai def initialize(input) @letters = [] @letters[1] = '.,!? ' @letters[2] = 'abc' @letters[3] = 'def' @letters[4] = 'ghi' @letters[5] = 'jkl' @letters[6] = 'mno' @letters[7] = 'pqrs' @letters[8] = 'tuv' @letters[9] = 'wxyz' @letters[0] = '' @input = input.gsub(/^0+/, '') if input.include?('0') end def input @input end def decipher if @input main_arr = @input.split('0').map do |item| number = item[0].to_i item_length = item.length letter_length = @letters[number].length if item_length > 0 and /^(\d)\1*$/.match(item) position = item_length % letter_length @letters[number][position - 1] end end end main_arr.join end end
僕がリファクタリングした後のコード
そして、上のコードを僕がこんなふうにリファクタリングしました。
class Keitai LETTERS = ['.,!? ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'].freeze def self.decipher(input) input .scan(/([1-9]+)0/) .flatten .map{|item| covert_to_letter(item) } .join end def self.covert_to_letter(item) letter_index = item[0].to_i - 1 position = item.length % LETTERS[letter_index].length - 1 LETTERS[letter_index][position] end end
Before/Afterだけ見ると、ほとんど原形をとどめていないように見えますが、決してフルスクラッチで書き直したわけではありません。
小さなリファクタリングを積み重ねていった結果、こうなりました。
スクリーンキャストの見どころ
では、このリファクタリングの過程をスクリーンキャストでご覧ください!・・・と言っても、僕がブツブツ言いながらリファクタリングしている様子を1時間半じ~っと見るのは苦行にしかならないと思います。
そこで、「このへんを見ると面白いかも?」というポイントをいくつかピックアップしてみます。
getterメソッドではなく、attr_readerを使う
API利用者の予想を裏切るような「サプライズメソッド」をなくす
仕様書に書かれていない「憶測に基づいた独自仕様」をなくす
正規表現を導入してコードをシンプルにする
- 試行錯誤開始: 1:01:20
- コード変更開始: 1:07:00
- コミット: Use regexp instead of split. · JunichiIto/Keitai-Message@3b7edcd · GitHub
contextを使ってspecを構造化する
クラスメソッドを導入して、APIをシンプルにする
すべてクラスメソッドにして、ステートレスなプログラムにする
RubyMineもフル活用しています!
あと、これはメインテーマではないのですが、コードはすべてRubyMineを使ってリファクタリングしています。
コードが自動補完されたり、変数名をリネームしたり、IDEならではの多機能さを見るのも面白いかもしれません。
おまけ: 動画を作った後もさらにリファクタリングしました
このスクリーンキャストを作った後に、「あ、まだリファクタリングできるやん」と思ったところがあったので2箇所ほど変更しています。
その1: 正規表現の先読みを使う
flattenがカッコ悪いので、flattenをなくす方法を考えてみました。
正規表現の先読み構文を使うと、1から9の文字だけをスッキリ取り出すことができますね。
(以下のコードはdiff形式で表示しています)
def self.decipher(input) input - .scan(/([1-9]+)0/) - .flatten + .scan(/[1-9]+(?=0)/) .map{|number_string| covert_to_letter(number_string) } .join end
その2: %wを使った配列リテラルに変更する
スクリーンキャストの中でも「ここは%wを使って書きたいな~」とつぶやいていたのですが、半角スペースをエスケープする方法がわからなくて、諦めていました。
後で調べてみると、バックスラッシュを使って「\(スペース)」と書けばいいんですね。
というわけで、%wを使った配列リテラルに変更してみました。
最初の「.,!?\ abc」のあたりが若干読みにくい気もしますが、短くなったのでとりあえず良しとします!
class Keitai - LETTERS = ['.,!? ', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz'].freeze + LETTERS = %w(.,!?\ abc def ghi jkl mno pqrs tuv wxyz).freeze def self.decipher(input) input
今回リファクタリングしたコードはこちら
今回リファクタリングしたコードはGitHubに置いています。
動画を見る時間がない人はコミットログを順に追っかけていくだけでも勉強になるかもしれません。
まとめ
いかがだったでしょうか?
上級者の方から見ると「ふーん」という内容だったかもしれませんが、Rubyを始めたばかりの人には「へ~、そんな書き方ができるのか」と思うところがたくさんあったんじゃないかと思います。
あと、今回は動画を使って実際に試行錯誤する過程を見てもらったり、リファクタリングの意図を口頭で話したりしたので、書籍やWeb記事では得られない「ニュアンス」みたいなものも感じ取ってもらえると嬉しいです。
題材募集!あなたのコードもリファクタリングします
今回初めてスクリーンキャストに挑戦しましたが、あと何本かこういうスクリーンキャストを作ってみようかなと思っています。
「自分の書いたコードもリファクタリングしてほしい!」という人がいれば、こんな感じでリファクタリングしてみますので声をかけてください。
応募条件は以下の通りです。
- 50~100行程度の短いRubyプログラムであること
- お題は自由です。良いお題がなければ僕から出題します。
- RSpecでテストが書かれていること
- テストがないとプログラムが壊れていないことを確認できないので。
- (自称)Ruby初心者であること
- リファクタリングの余地がない完璧なコードを渡されても困るので。。。
Twitterのメンションなり、コメント欄なり、ページ右下のMessageLeafなり、お好きな方法で声をかけてください。
次にやるときは僕が一人でしゃべるのではなく、元のコードの作者とSkypeとかで会話しながらスクリーンキャストを録画したいな~と思っています。
いや、SkypeよりもsgSCREENを使うのが今ドキかなっ!?
何はともあれ、みなさんのご応募お待ちしております!!
あわせて読みたい
[初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか
Rubyを始めて間もない方にはぜひ読んでもらいたい入門記事です。
この記事を参考にしながら自分のコードを見れば、それだけでリファクタリングできる箇所がいくつも出てくると思います。
Rubyをより良く書けるようになるための課題演習のつくりかた | mah365
会社の同僚、西見まーくん(@mah_lab)が昨日公開したブログエントリです。
僕が今回やったリファクタリングもこのエントリと同じカテゴリに入りますね。
お知らせ その1: 西脇.rb & 東灘.rbでは毎月勉強会をやっています!
西脇.rb & 東灘.rbは、僕とAkiさん(@spring_aki)で主催しているのRubyコミュニティです。
毎月神戸近辺で勉強会を開いています。
神戸ぐらいなら足を運べそう、という方はぜひDoorkeeperのサイトからメンバー登録してください!
http://nishiwaki-higashinadarb.doorkeeper.jp/
ちなみに次回は5月末の開催を予定しています。
お知らせ その2:「Everyday Rails - RSpecによるRailsテスト入門」もよろしくお願いします
僕が翻訳したRSpec関連の電子書籍です。
初心者向けの易しい内容になっていますので、「題材募集に応募したいけどRSpecは苦手」という方はぜひこの本で勉強してみてください!
(ただし、Railsアプリケーションをテストするコードが中心です)