新卒だからこそ古典から設計を学ぶ
中村 真人
こんにちは、テスト自動化チーム新卒の中村(@m_nakamura145)です。
今回は、ソフトウェア設計について悩んでいたときに、本を読んで参考になった考え方を紹介します。
問題
いきなりですが、今回は例として、野球のストライク、ボール、アウトの個数(スコアカウント)を計算するプログラムを書いてみます。
B
とS
という文字が任意の個数(BBSSSBSBSSS
のような文字列)で入力され、スコアカウントのHash
を返すプログラムです。
この問題に対して、私は最初にこんなRubyコードを書きました。
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 |
class BaseBall def initialize(game_status) @game_status = game_status.chars.to_a @count = { strike: 0, ball: 0, out: 0 } end def calculate_score until @game_status.empty? ball = @game_status.shift if ball == "B" @count[:ball] += 1 elsif ball == "S" @count[:strike] += 1 end if @count[:ball] == 4 @count[:strike] = 0 @count[:ball] = 0 end if @count[:strike] == 3 @count[:strike] = 0 @count[:ball] = 0 @count[:out] += 1 end if @count[:out] == 3 @count[:out] = 0 end end @count end end |
calculate_score
メソッドにスコアカウントに必要な処理を全て詰め込んでいる書き方です。
このプログラムは動きますが、一つのメソッドに複数の処理がベタ書きされており、パッと見で何の処理が書いてあるかわかりづらいです。
もし、追加で「H
という文字が入力されたらヒットとして、スコアカウントをリセットせよ」のような仕様の変更があった場合、 calculate_score
メソッドの処理は、さらに複雑になってしまいます。
私は、このプログラムを読みやすく、拡張しやすいプログラムにするためにはどういう設計をすればいいのか悩んでいました。
読みやすいプログラムを書くためには?
プログラムを書いていて自分が書いたプログラムがどんどん膨れ上がり、読みづらいプログラムになっていくのはよくあることだと思います。
ひたすらプログラムを書き続けるだけでは自分の持っている知識の中でしか設計できず、設計力は向上しません。
そこで、UNIXという考え方という本を手に取りました。
UNIX哲学
この本は、UNIXオペレーティングシステムの設計の背後に隠された思想、UNIX哲学について書かれた本です。原著は1994年に書かれています。
この本では、UNIX哲学は9つの大定理から成り立つと書かれています。
今回は、その中からソフトウェア設計に関して重要だと感じた定理を2つ紹介します。
スモール・イズ・ビューティフル
「小さいプログラムには大きなプログラムよりもメリットがある」というのは、
UNIX哲学以外の場面でも言われることではないでしょうか。
ある大きな問題を分解し、複数の小さい問題に小分けして、
その小さい問題を処理するプログラムを組み合わせていくいう考え方です。
本書では、実行可能なプログラムという文脈で上記の内容が書かれていましたが、
これはメソッド単位の設計でも同様のことが言えると思います。
100行のメソッドよりも、5行のメソッドの方がバグが発生する確率は低くなります。
小さいメソッドは人間が理解しやすく、保守しやすく、他のメソッドと組み合わせやすいです。
一つのプログラムには一つのことをうまくやらせる
あるプログラムにはひとつの処理をさせる。
言うのは簡単ですが、実際に実現しようとすると、入力値のバリデーションを行ったり、
エラーが起きた時の処理を書いたりして、本来やりたかったことの何倍ものプログラム量になり、
理解するのが難しくなります。
しかし、一つのことをうまくやるようなプログラムが書けると、それは自然に小さなプログラムになります。
UNIX哲学の考え方を適用する
野球のスコアカウントを計算するためには、以下の手順が必要です。
- 投げられたボールの取得
- ボールの判定(ストライクか?ボールか?)
- フォアボールの判定
- 三振の判定
- スリーアウトの判定
- スコアカウントのアップデート
最初のプログラムでは、上記の手順を全てcalculate_score
メソッドの中で行っていました。
UNIX哲学の考え方に従って、小さく、一つのことをうまくやるメソッドを作ってみましょう。
メソッドの切り出し方に注目してください。
新しいcalculate_score
メソッドは以下のようになりました。(initialize()
内の処理は同じなので割愛)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def calculate_score until @game_status.empty? judge if four_balls? score_when_four_balls end if out? score_when_out end if three_out? score_when_three_out end end @count end |
calculate_score
メソッドの中の処理が非常にシンプルになったと思います。
分割したメソッドも適切な名前がつけられており、メソッドの中でどんな処理が行われているか、理解しやすくなっていると思います。
分割したメソッドの中身は以下のようになっています。
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 next_ball @game_status.shift end def ball?(ball) ball == "B" end def strike?(ball) ball == "S" end def judge ball = next_ball if ball?(ball) @count[:ball] += 1 elsif strike?(ball) @count[:strike] += 1 end end def out? @count[:strike] == 3 end def four_balls? @count[:ball] == 4 end def three_out? @count[:out] == 3 end def score_when_four_balls @count[:strike] = 0 @count[:ball] = 0 end def score_when_out @count[:strike] = 0 @count[:ball] = 0 @count[:out] += 1 end def score_when_three_out @count[:out] = 0 end |
それぞれ数行のメソッドになっており、何を処理しているのか非常に理解しやすくなりました。
このように、一つ一つ小さなメソッドを組み合わせていくことで、理解しやすく、保守しやすいプログラムになります。
歴史に学ぶ
UNIXのような長期間、広範囲で使われているソフトウェアには一貫した設計思想が存在します。
良い設計を学ぶためには、設計者がどういう考えで設計を行っているのかを理解することが非常に重要です。
その際に古典と呼ばれる本を読むことは非常に有効だと思います。
「UNIXという考え方」は1994年に書かれた本ですが、その内容は先ほどの定理のように、現代のソフトウェア開発にも十分通用する内容だと思います。
おわりに
UNIXの設計思想は、「プログラムは小さく、シンプルに」というものでした。
小さくシンプルなプログラムは、理解や保守が簡単になります。
ソフトウェア設計は体系的な知識が必要になる分野だと思います。古典を読んで体系的なコンピュータサイエンスの知識を身につけ、最高のソフトウェア設計を目指しましょう。
最近、私はオブジェクト指向プログラミングの正しいやり方を学びたいと思い、オブジェクト指向に入門しています。
皆さんもソフトウェア設計に悩んだときは立ち止まって、設計思想について深く考えてみてはいかがでしょうか?