初心者が作り出したRubyの失敗コード集(非オブジェクト指向編)
現在受講しているプログラミングスクール「FJORD BOOT CAMP(フィヨルドブートキャンプ)」では、カリキュラムの中に「lsコマンドをRubyで作る」という提出課題があります。
🔗 FJORD BOOT CAMP(フィヨルドブートキャンプ)
# lsコマンドをRubyで実装する $ ls.rb Directories empty_file file_3 directory_1 file_1 directory_2 file_2 $ ls.rb -alr ~/samples total 40 -rw-r--r-- 1 user group 4097 1 17 15:21 file_3 -rw-r--r-- 1 user group 4096 1 17 15:21 file_2 -rw-r--r-- 1 user group 1 1 17 15:21 file_1 -rw-r--r-- 1 user group 0 1 17 15:21 empty_file drwxr-xr-x 2 user group 64 1 17 15:21 directory_2 drwxr-xr-x 2 user group 64 1 17 15:21 directory_1 drwxr-xr-x 5 user group 160 1 17 15:21 Directories -rw-r--r-- 1 user group 1 1 17 15:21 .file drwxr-xr-x 2 user group 64 1 17 15:21 .directory drwxr-xr-x 59 user group 1888 1 17 15:35 .. drwxr-xr-x 11 user group 352 1 17 15:22 .
lsコマンドのカリキュラムは学習の前半と後半でそれぞれ用意されており、前半は「非オブジェクト指向版」、後半は「オブジェクト指向版」で実装・提出します。
(優秀な方は前半のlsコマンドもオブジェクト指向で提出されていらっしゃるようです…!)
自分は先日、前半の「非オブジェクト指向版」を、長い長い時間をかけてようやく修了いたしました…😭
(おそらくフィヨルドブートキャンプ史上最も遅い到達者だと思います🐢)
この記事を書いたきっかけ
最初に提出したlsコマンドは「必須要件を満たしただけ」のコードでした…😅
(RuboCopの網を奇跡的にくぐり抜けたようなコード)
しかしフィヨルドブートキャンプの提出課題は「必須要件さえ満たしていればOK!」ではなく、「コードの書き方(利用者目線で書かれているか、読み手に意図が伝わりやすいコードかどうか等)」も合格or再提出の基準となっています。
最初に提出したコードから、メンターさんの様々なアドバイスによって「劇的ビフォー・アフター」が何度も起き、修了時には自分自身のコードの書き方・考え方も劇的に変わる「奇跡体験アンビリバボー」な学習体験となりました。
そこで今回、同じカリキュラムに取り組んでいる方も気兼ねなく読めるようネタバレを回避しつつ、スクール以外のRuby初学者さんにも読んでいただけるよう今回学んだことの一部を『失敗コード集』という形でまとめてみました。
自分の数々の失敗が、Ruby初学者さんの反面教師になれると嬉しいです😆
対象読者さん
- Ruby初学者の方
- スクールの課題に依存したコードは書いておりませんので、「Rubyの入門本を一冊読み終わったど〜」という方はもれなく対象読者さんです😆
- フィヨルドブートキャンプでRubyプラクティスを進めている同志の方
- ネタバレになりそうなコードは避けてあるので安心して読み進めてください😆
- ※サンプルコードはlsコマンドと1ミリも関係ない「サザエさんの世界」に置き換えてあります
- ネタバレになりそうなコードは避けてあるので安心して読み進めてください😆
最初にお詫び
この失敗コード集は「オブジェクト指向ではないRubyコード」が元になっています。そのため、オブジェクト指向の場合は書き方そのものが一気に変わるかもしれません。
(この記事の執筆者はまだオブジェクト指向で思考できておりません…すみません😣)
コードの書き方に不適切な箇所があった際は忌憚なくご指摘いただけますと幸いです🙇🏻♂️ すぐに修正させていただきます。
※長谷川町子美術館様へ
この記事では、Rubyというプログラミング言語のサンプルコード(プログラム例)の中で「サザエさんの世界」をお借りしています。
もしも著作権侵害や不快な内容となっていた際は大変申し訳ございません、すみやかに差し替えますのでその旨お知らせいただけますと幸甚です。
定数をコードの途中で定義していた
ビフォー :
# ...いろいろな処理... def chase(target, footwear: nil) # ... end MAIN_CHARACTER = 'サザエ'
定数を「コードの途中」で書いていました。
慣習ではない上、読む人にとって「次にいつまた定数が現れるか分からないプログラム」になってしまいます😣
学んだこと
- 定数はファイルの上部(
require
があればその下辺り)で定義する
アフター :
require 'sazaesan' MAIN_CHARACTER = 'サザエ' LAST_NAME_TABLE = { namihei: '磯野', sazae: 'フグ田', norisuke: '波野', karu: '伊佐坂' # ... } REGULAR_PETS = %w[tama hachi] def chase(target, footwear: nil) # ... end # ...
定まっていない値を定数にしていた
SPECIFIED_FAMILY = ARGV[0]
コマンドラインから受け取った引数を「定数」として定義していました。「定数とは…」という失敗例です😣
学んだこと
- 定まっていない値は「変数」が向いている
- 定数は文字通り「定まっているもの」に対して使う
# 定まっていない値は変数が向いている specified_family = ARGV[0] # 「定まっているもの」は定数が向いている IKURA_VOCABULARIES = %w[はーい ちゃーん ばぶー].each(&:freeze).freeze # ※マジックナンバーは積極的に定数にする NUMBER_OF_EPISODES_PER_WEEK = 3
理解していない例外処理を「御守り感覚」で書いていた
exit if __FILE__ != $PROGRAM_NAME
上記は「何か予期せぬトラブルを防いでくれるかも」と、どこかで見たコードを御守り感覚で記述したものです。
(自分のコードにこの記述は不要でした😣)
学んだこと
- 必要かどうかまず考える
- コードの選択肢には「書かない」がある
- 書くからには責任を持つ
- 責任を持つ ≒ そのコードに対してテストが書ける
コンテキストを考えず唐突に「何が入っているのか分からないコード」を書いていた
ビフォー :
sport = ARGV[0] # ...いろいろな処理... # ... # ...さらにいろいろな処理 # ... puts "おーい磯野、#{sport}しようぜ"
当初、「変数は上部でまとめておいた方が良いのかな」と上記のような書き方をしていました。その結果、読む人にとって「行ったり来たりするコード」になってしまいました😣
学んだこと
- コンテキストを常に考えて「伝える努力」を怠らない
- 使用する直前でローカル変数に代入するなど
アフター :
sport = ARGV[0] puts "おーい磯野、#{sport}しようぜ"
いろいろな処理を経て作った配列のブロック変数に「番号指定パラメータ」を使っていた
ビフォー :
complexes.shift.zip(*complexes).map { _1.join("\t") }
複雑な配列(配列の配列など)のブロック変数に対し、番号指定パラメータを用いていました。この場合ブロック変数の中身は何なのか、読む人に負担を強いてしまいます😣
学んだこと
- 「意味を持つ名前」にする
- 名前は「中身を伝える大切なコードの1つ」
アフター :
# 中身が「名前の入った配列」なら「names」にする等 complexes.shift.zip(*complexes).map { |names| names.join("\t") }
他のメソッドに依存したメソッドや、引数や戻り値を活用しないメソッドを作っていた
ビフォー :
def first_line @string = 'お魚くわえたドラ猫追っかけて' end def first_and_second_lines @string += "\n裸足でかけてく陽気なサザエさん" end
first_line puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん
メソッド間の一時的なデータに「インスタンス変数」を用いて不必要に変数の寿命を延ばし、且つ、引数も戻り値も活用しない「再利用しづらいメソッド」を作っていました😣
上記のコードは最初のメソッドに依存しており、2番目のメソッドだけを呼び出すと例外が発生してしまいます。
puts first_and_second_lines
#=> NoMethodError (undefined method `+' for nil:NilClass)
さらに上記のコードはインスタンス変数を破壊的に変更しているため、順番通りにメソッドを呼び出した後、
first_line puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん
2番目のメソッドをもう一度呼び出すと、返ってくる値が変わってしまいます。
puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん
今回は「何度呼び出しても同じ結果が得られるメソッド」にする必要があるため、非常に都合が悪くなってしまいます😣
# 心配なサザエさん puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん # 裸足でかけてく陽気なサザエさん
学んだこと
- 呼び出す順番に依存したメソッドは作らない
- 何度呼び出しても同じ結果が得られるメソッドを作る
- メソッドの戻り値と引数を活用して再利用できるように作る
アフター :
def first_line(item, target) "#{item}くわえた#{target}追っかけて" end def second_line(state, chaser) "裸足でかけてく#{state}な#{chaser}" end
# 単体でも呼び出せるようになった puts second_line('陽気', 'サザエさん') #=> 裸足でかけてく陽気なサザエさん
# 再利用もできるようになった first = first_line('ノリスケ', '波平') second = second_line('能弁', 'イクラちゃん') puts first, second #=> ノリスケくわえた波平追っかけて # 裸足でかけてく能弁なイクラちゃん
# 何度呼び出しても同じ結果が得られるようになった first = first_line('お魚', 'ドラ猫') second = second_line('陽気', 'サザエさん') first_and_second_lines = [first, second] puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん puts first_and_second_lines #=> お魚くわえたドラ猫追っかけて # 裸足でかけてく陽気なサザエさん
長いif〜else〜end
を作っていた
ビフォー :
作ったif式が長くなり、else
とかなり距離が離れてしまったため、
if specified_family == '磯野家' # # # ...いろいろな処理... # # else #... end
行ったり来たりせずに済むのではと思い、else
からelsif
に置き換えました。
if specified_family == '磯野家' # # # ...いろいろな処理... # # elsif specified_family != '磯野家' #... end
たしかにelse
が何を指しているかはその場で伝わるかもしれませんが、これだとelsif
を適切に使っていない上、elsifの条件式そのものが冗長です😣
学んだこと
- 条件分岐の処理はできる限りシンプルにする
- 条件ごとの処理が長い場合は「メソッド分割」のサイン
アフター :
if specified_family == '磯野家' dive_to_house('磯野家') else # ... end # 処理をメソッド化 def dive_to_house(specified_family) # # # ...いろいろな処理... # # end
なんでもeach
でやり繰りしていた
ビフォー :
names = %w(namihei katsuo norisuke tarao) # 指定した文字で始まる名前が取得できるメソッド def names_starting_with(names, initial) matched_names = [] names.each do |name| matched_names << name if name.start_with?(initial) end matched_names end # `n`から始まる名前を取得 names_starting_with(names, 'n') #=> ["namihei", "norisuke"]
とにかくeach
で書き、便利なメソッドを知る機会を自ら逸していました😣
学んだこと
- 「
eahch以外での実装方法
」を考えてみる- いろいろなメソッドを習得するチャンス
アフター :
# mapの例(配列への格納と返却が不要に) def names_starting_with(names, initial) names.map do |name| name if name.start_with?(initial) end.compact end
# filter_mapの例(さらにcompactも不要に) def names_starting_with(names, initial) names.filter_map do |name| name if name.start_with?(initial) end end
# selectの例(さらにifも不要に) def names_starting_with(names, initial) names.select do |name| name.start_with?(initial) end end # 1行表示の場合 def names_starting_with(names, initial) names.select { |name| name.start_with?(initial) } end
each以外の方法
に挑戦することで、手を動かしながら「便利なメソッド」をどんどん習得できるようになっていきました😂
外で書いても機能する繰り返し処理を「繰り返し処理の中」で書いていた
ビフォー :
names = %w[namihei fune ikura] # 最大文字長の名前を基準に、任意の文字で桁揃えするメソッド def names_filled_with(names, char) names.map do |name| name.ljust(names_max_length(names), char) # 二重の繰り返しが発生😣 end end # 最大文字長を取得するメソッド def names_max_length(names) names.max_by { |name| name.length }.length end puts names_filled_with(names, '!') #=> namihei # fune!!! # ikura!!
繰り返しの外でも動く処理を「繰り返し処理の中」で書いていました。
もしも1万件のデータが入っていた場合、単純計算で「1万回 × 1万回」の処理が走ることに…😣
学んだこと
- 繰り返し処理では「不必要に繰り返し処理を書いていないか」を意識する
- 外側で動くものは外側に移動する
アフター :
def names_filled_with(names, char) max_length = names_max_length(names) # 繰り返し処理の外側に移動 names.map do |name| name.ljust(max_length, char) end end
puts names_filled_with(names, '!') #=> namihei # fune!!! # ikura!!
最後に(失敗コードから得られたもの)
今回の数々の失敗を通じて、コードの書き方・考え方がガラリと変わりました。「下の下」から一気に「下の上」までレベルアップした感じです。
もしもスクールの合格基準が「必須要件さえ満たしていればOK!」であれば、最初の提出ですでに必須要件を満たしていたので「良いコードなのか悪いコードなのか」も分からず、次に進んでいたと思います…
「コードの向こうには人がいる」ことを大切にしているスクールに参加できて、本当に良かったな〜と感じています🙂
また、メンターさんのアドバイスを受けながら、自分で作ったコードを自分でリファクタリングしていく工程は「その場で効果が分かる」「お墨付きの書き方」ということもあり、学んだ知識が定着しやすいことも実感しました。
これからも自分は無数の失敗を繰り返していくと思いますが…、 涙の数だけ 失敗の数だけ強くなりたいと思います💪
ということでRuby初学者の皆さん、失敗を恐れず一緒にRubyistを目指していきましょう😆
P.S. オブジェクト指向版のlsコマンドで再びお目にかかりたいです
次回のlsコマンドに辿り着くまで「27のカリキュラム」が残っているのですが…、再び同じような形で「失敗コード集(オブジェクト指向編)」が執筆できればと思っています。頑張ります…!
【謝辞】
自分の闇鍋のようなコードを合格レベルまで導いてくださった伊藤メンター、本当にありがとうございました!
以上、最後までお読みいただきありがとうございました🙇🏻♂️