はじめに
タイトルは半分釣りです。ごめんなさい。
先日、僕のブログで以下のようなエントリを書きました。
アウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題を集めてみた(全10問) - give IT a try
簡単にいうと、技術書の丸写しをしてアウトプットした気にならず、自分でコードを書いてそれを公開しようぜ〜という内容です。
で、そのお題となるような簡単なプログラミング問題を10問ほどブログ内で紹介しています。
これまで何人かの方がご自身のブログやQiitaに解答コードを載せているのを見かけました。
ちゃんとチャレンジして偉い!すばらしい!!👏👏👏
・・・と、実行に移した姿勢は非常に良いのですが、コードを見てみると多くの方が「うーん、惜しい!」と思ってしまう「ある病気」にかかっていました。
というわけで、この記事ではその「ある病気」について、それとプログラミングにおける設計の重要性について説明していきます。
(そして、本記事のタイトルが意味するところは、この記事を最後まで読めばわかります!)
初心者さんがかかりやすい病気=「puts病」
はい、その病気はputs病です。
この病気は与えられたコンソールプログラム問題を、逐一puts
(もしくはprint
)で出力してしまう病気です。
たとえばこんな感じですね。
(このコードは前述のブログ記事とは無関係な、あまり意味のないコード例です。)
puts 'hoge hoge'
[1, 2, 3, 4, 5, 6].each do |item|
if item <= 5
print item
end
end
print "\n"
puts 'piyo piyo'
上のコードを見てもらうとわかるとおり、プログラムの実行中に何度もputs
やprint
が登場しています。
念のため、実行結果も載せておきます。(この結果も特に意味はありませんが)
hoge hoge
12345
piyo piyo
たしかに、これでもコンソール上で実行したときは要件を満たす出力が得られます。
ですが、puts
を多用してしまうと以下のような問題が発生します。
問題1. テストを自動化しづらい(=リファクタリングしづらい)
問題の解答として得られる結果をputs
で出力してしまうと、テストコードを書きづらくなります。
たしかに、やろうと思えばMinitestやRSpecで標準出力の出力内容をテストすることはできます(以下はMinitestのコード例です)。
# コンソールに出力されるはずの文字列
expected = <<TEXT
hoge hoge
12345
piyo piyo
TEXT
# do_somethingを呼んだら、上の文字列がコンソールに出力されることを検証する
assert_output(expected) { do_something }
ですが、これだと何かにつけて「文字列のかたまり」として結果を検証する必要があるため、小回りが利きません。
たとえば、配列として内容を検証したいと思ったり、出力結果の一部を数値や日付として検証したいと思ったりしたときに、とても面倒なことになります。
そもそも、puts
でコードを書いた人は、コードが動いたことに満足してしまって、おそらくテストコードを書いてないと思います。
もし僕が「じゃあ、テストコードも追加してもらえますか」とお願いしたら、きっと「えっ・・・」と固まってしまうことでしょう。
テストコードを書いていないと、(自分は目視で絶対に結果が変わっていないことを100回でも1000回でも確かめられる!という人以外は)リファクタリングもできないので、それ以上コードをきれいにすることも困難になります。
問題2. コンソール以外で再利用しづらい
今回はコンソールプログラムとして動かすことがお題になっていますが、もし同じロジックをRails上でも使いたいと思ったらどうしますか?
puts
を使っているとおそらくそのままでは流用できないので、コピペした上で修正するか、Rails用にゼロから再実装することになると思います。
せっかく実装したコードなので、コンソールであろうとWebであろうと、どこでも使えるようにしたいと思いませんか?
でも、puts病にかかっていると、それができないのです。
解決策:ロジック本体と画面出力をきちんと分離すべし
上で述べた問題(特に後者)の最大の原因は、ロジック本体と画面出力が密結合してしまっているためです。
つまり、要件を満たすための解法と、それを表示する仕組みがべったりと一体化してしまっているために、そのコードを実行するための前提条件が厳しく制限されているのです。
これを解消するためにすべきことは、ロジック本体と画面出力を分離することです。
そうすれば、ロジック本体だけをRailsで再利用することもできますし、テストコードを書くときもロジック本体の戻り値をテストすればよいことになります。
先ほど見せた「puts病」のプログラムをロジック本体と画面出力に分離すると、次のようなコードになります。
puts
が1ヶ所しか登場していない点に注目してください。
def main
# ロジック本体から画面出力に依存しない形で結果を受け取る
rows = do_something
# コンソール用に結果を整形して出力する(配列の内容をputsで出力するだけ)
puts rows
end
# ロジック本体。要件を満たす配列を戻り値として返す
def do_something
rows = []
rows << 'hoge hoge'
cols = []
[1, 2, 3, 4, 5, 6].each do |item|
if item <= 5
cols << item
end
end
rows << cols.join
rows << 'piyo piyo'
rows
end
# ファイルを直接実行されたときだけ、mainメソッドを起動するイディオム
# 参考 http://blog.10rane.com/2015/03/26/meaning-of-the-code-dollar0-equal-__file__/
if __FILE__ == $PROGRAM_NAME
main
end
こうすれば以下のようにテストコードも書きやすくなります。
require './do_something'
require 'minitest/autorun'
class DoSomethingTest < Minitest::Test
def test_do_something
assert_equal ['hoge hoge', '12345', 'piyo piyo'], do_something
end
end
もちろん、コンソールプログラムとして実行することもできます。
$ ruby do_something.rb
hoge hoge
12345
piyo piyo
また、テストコードを書いているので、リファクタリングも自信をもって行えます。
def do_something
# 元のコードをシンプルにリファクタリングしたバージョン
[
'hoge hoge',
[1, 2, 3, 4, 5, 6].select{|n| n <= 5}.join,
'piyo piyo'
]
end
つまるところ、これはMVCパターン!
先ほどは「ロジック本体と画面出力の分離」と書きましたが、この考え方は結局、RailsのMVCパターンと同じです。
具体的にMVCを分析してみましょう。
-
do_something
メソッドはModelを返すメソッドです。 -
puts rows
はViewのレンダリングです。 - そして、
main
メソッドがControllerです。
これをふまえて先ほどのmainメソッドをもう一度見直してみると、そのことがわかると思います。
# mainメソッドはController
def main
# 戻り値のrowsがModel
rows = do_something
# Modelの内容をViewとしてレンダリング
puts rows
end
一番のポイントはModelをView(ここではコンソール)に依存させず、純粋なRubyオブジェクト(Plain Old Ruby Object = POROとも呼ばれる)として表現することです。
ModelがPOROになっていれば、テストコード上でもテストしやすいですし、コンソールであろうとWebであろうと、核となるロジックそのものはいつでもどこでも再利用できるようになります。
もしRailsに移植したら?を自問自答してみる
いったんコンソールプログラムを書き上げたら、「もしこれをRailsに移植しろと言われたらどうするか?」を自問自答してみてください。
そして、どこからどこまでがModelの責務で、どこからどこまでがViewの責務になるかを考えてみましょう。
(対話型のコンソールプログラムであれば、Controllerに相当する責務もある程度発生すると思います)
「このロジックはRailsでもそのまま再利用できそう(またはそのまま再利用できるようにすべきだ)」と感じた部分がModelです。
「この処理はたぶんERBやHamlに書くだろうな」と思った部分がViewです。
頭の中でその2つを分離できたら、コンソールプログラム上でもその2つを明確に分けて実装するようにリファクタリングしてみましょう。
Q. 最初のコードみたいにputsを多用するのはダメですか?
いいえ、絶対にダメ、というわけではありません。
いきなりMVCを意識してコードを書くのはプログラミング初心者さんにはなかなかハードルが高いと思います。
ですので、最初はputsを多用する形で解いてみて、それから次の段階として「どうすればロジック本体と画面出力を分離できるか」「どうすればテストコードが書きやすくなるか(そもそもどんなテストコードを書けばいいのか)」といったポイントを検討するのがよいと思います。
慣れてくるとだんだんと最初からMVCを意識しながらロジックが考えられるようになります。
そして、「こんなRubyオブジェクトが戻り値として返ってくればよさそうだ」という見当が付くと、テスト駆動開発(TDD)で問題を解くことも可能になります。
設計力を身に付けて脱初心者を目指そう
プログラムとして解くべき課題を与えられたときは、やみくもにコードを書き始めるのではなく、まず「プログラムのあるべき姿や、あるべきデータ構造」を頭の中でイメージできるようになりましょう。
もちろん、頭の中だけでなく、紙の上に書き出すのもOKです。
実際にコードを書き始める前に行うこのような作業のことを「設計(design)」といいます。
そして、適切な設計を考えられるスキル(設計力)は「脱初心者」を目指す上で非常に重要なスキルの一つです。
ただ、このスキルは独学で習得しようとするとかなり苦労する(何が正解か自分ではわかりにくい)と思うので、できれば熟練者に助言をもらいながら学習するのが理想的です。
まとめ
というわけで、この記事ではたとえ簡単なコンソールプログラムであっても、MVCを意識してテスト容易性やプログラムの再利用性を検討することの重要性を説明してみました。
Railsを勉強していると必ず「MVCパターン」という言葉に遭遇していると思います。
「MがModel、VがView、CがControllerか。よし、完全に理解した!」と思っていても、こういったプログラミング問題でModelとViewが密結合したプログラムを書いてしまうのは、もしかするとMVCパターンをまだ完全には理解できていない証拠かもしれません。
コンソールプログラムを書くときにputs
やprint
を多用してしまう人は、MVCを意識しながら下記ブログの問題にチャレンジしてみてください😃
アウトプットのネタに困ったらこれ!?Ruby初心者向けのプログラミング問題を集めてみた(全10問) - give IT a try
あわせて読みたい:チェリー本の例題もPOROで実装しています
拙著「プロを目指す人のためのRuby入門」(通称・チェリー本)をお持ちの方は、一度各章に載っている例題の解答を見直してみてください。
どの章も解答例は純粋なRubyオブジェクト(PORO)を戻り値として返すように実装しているはずです(2章と9章を除く)。
Minitestでテストコードを書きながらテスト駆動開発できているのは、要件を満たすコードをPOROとして実装しているおかげです。
ModelとViewをどう分離すればわからない人は、チェリー本の例題を再確認してみるのも良いかもしれません。
参考までに、第5章の「単位変換プログラム」の解答例を載せておきます。
UNITS = { m: 1.0, ft: 3.28, in: 39.37 }
def convert_length(length, from: :m, to: :m)
(length / UNITS[from] * UNITS[to]).round(2)
end
require 'minitest/autorun'
require './lib/convert_length'
class ConvertLengthTest < Minitest::Test
def test_convert_length
assert_equal 39.37, convert_length(1, from: :m, to: :in)
assert_equal 0.38, convert_length(15, from: :in, to: :m)
assert_equal 10670.73, convert_length(35000, from: :ft, to: :m)
end
end