Eat, Play, Nap and Code

食とあそびと昼寝とプログラミング学習

【リーダブルコード】自作プログラムの無関係の下位問題抽出に取り組みたい

f:id:eatplaynap329:20211206024357p:plain

はじめに

こんにちは。これはフィヨルドブートキャンプ Part 1 Advent Calendar 2021の6日目の記事です。 フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventarもあります。

5日目はharuna (id:napple29)さんのオンラインでのコミュニティ参加についての記事でした。

napple29.hatenablog.com

今となってはフィヨルドに馴染むというのはなんだかしっくり来ず、色々と参加している人が側から見ると馴染んでいるように見えるだけなのでは、と思います。

本当に自分もそう思います…。

私も今でこそコミュニティに積極的に参加している方ですが、最初に質問タイムに入ったときは、ガチガチに緊張してペアプロしてもらってる1時間ずっと正座していましたし、(輪読会を通じて知り合いができるまで)ミートアップは話すこともないし話にもついていけないからほとんど行ってませんでした。

多分ほとんどの人はそうだと思うので、億劫でなければ何かに一度飛び込んでみるのはそんなに怖くないって知っておいてほしいし、誰も話す人がいないから参加しづらいって方は私に話しかけてくれたらいいと思いますよ😄

ちなみに12/18(土)の初めてのLT会vol.10は私も登壇するのでよろしくおねがいします🙏

もくじ

自己紹介

トミーと申します。インターネット上ではeatplaynapもしくはeatplaynap329というアカウント名で活動しています。

2020年10月1日にフィヨルドブートキャンプに入って1年以上が経過しました。 現在はJavaScriptのプラクティスをやっています。

去年のアドベントカレンダーにも参加していて、あれからDuolingoの継続日数は900日を超えましたが、卒業はまだ遠そうです…😇

eatplaynap329.hatenablog.jp

無関係の下位問題抽出 is what

以前こんな記事を書いたんですが、現在私は2つの『リーダブルコード』の輪読会に参加しています。

eatplaynap329.hatenablog.jp

(日本語で読む会を主催したところ、英語で読む会も爆誕したのでそっちにも参加している)

2つの輪読会両方にコンスタントに参加しているのは今のところ私だけなので、おそらく現在フィヨルドブートキャンプ内で1番リーダブルコードを読み込んでいる人間なのではないかと思います。

読んでいる中で、どうしても気になったのが10章の「無関係の下位問題を抽出する」のこんな文面です。

ほとんどのコードは汎用化できる。一般的な問題を解決するライブラリやヘルパー関数を作っていけば、プログラムに固有の小さな核だけが残る。
この技法が役に立つのは、プロジェクトの他の部分から分離された、境界線の明確な小さな問題に集中できるからだ。こうした下位問題に対する解決策は、より緻密で正確なものになる。それに、あとでコードを再利用できるかもしれない。

Dustin Boswell/ Trevor Foucher『リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)』角 征典訳、オライリージャパン、2012年、141頁

つまり、些末な詳細をどんどん切り分けていけば、そのプログラムによって本当に解決しなくてはいけない問題だけが残るので、集中して取り組むべき点が明確になるということだと理解しました。なんて美しい考え方なんだ!

そのために、コードを書くなかで「無関係の下位問題」がないかと常に考え、あれば積極的に関数に切り出すことが推奨されています。

自分はRubyでもJSでも関数への切り出しを積極的にやっていないので、後で自分の書いたものを読むと読みづらいなあと思うことがしばしばあります。

しかしリーダブルコードガチ勢として、もっとリーダブルなコードが書けるようになりたい。

なのでこの記事では自分の書いた簡単なプログラムを、関数への抽出という観点からセルフリファクタリングしていこうかと思います。

書くプログラム

Node.jsでコマンドラインで動く「クリスマスプレゼント選定プログラム」を書いてみようと思います。 流れとしては↓のような感じ。

  1. 標準入力から年齢を受け取る
  2. 15歳以下かどうか判定
  3. 15歳以下だったらJSONファイルから年齢に応じたおもちゃをプレゼント
  4. 16歳以上だったらMath.random()で計算した金額をプレゼント
  5. プレゼントをコンソールに出力する

ちなみに自分は10歳までサンタクロースを信じていて、10歳のクリスマスプレゼントが電子辞書だったことに不信感を抱いて親を問い詰めたら「いつまで信じているんだろうとヒヤヒヤしていたのでやっと気づいてよかった」と言われました🤔

本編

ベタ書きで書いてみる

年齢と対応するプレゼントのデータが入ったJSONファイルをつくり、思いつくままコードを書いてみました。

#! /usr/bin/env node

const fs = require('fs')
console.log('年齢を教えてね')
const input = parseInt(fs.readFileSync('/dev/stdin', 'utf8'))

if (input < 0  || isNaN(input)) {
    console.log('HOHOHO 正しい年齢を入力してね')
} else if (input <= 15) {
    const presents = JSON.parse(fs.readFileSync('gifts.json', 'utf8'))
    const result = presents.find(present => present.age === input)
    console.log(`メリークリスマス!${result.name}を贈ります${result.emoji}`)
} else {
    const gift = Math.floor(Math.random() * 100) * Math.floor(Math.random() * 100)
    console.log(`メリークリスマス!${gift}円を贈ります🎉`)
}

↑を実行すると、標準入力で受け取った数値を元にJSONファイルの検索が行われ、↓のような結果が得られます。

❯ ./ver1.js
年齢を教えてね
0
メリークリスマス!ぬいぐるみを贈ります🧸

❯ ./ver1.js
年齢を教えてね
15
メリークリスマス!ノートパソコンを贈ります💻

❯ ./ver1.js
年齢を教えてね
28
メリークリスマス!4940円を贈ります🎉

❯ ./ver1.js
年齢を教えてね
-1
HOHOHO 正しい年齢を入力してね

関数もないし同期的な処理なので、上から順に処理させるだけです。

簡単なプログラムなのでこのままでも別に読みにくくて大変とは思わないんですが、工夫してもっと読みやすくできればいいな。がんばります。

関数を導入

先程のコードは上から順に処理を書いているだけだったので、標準入力から年齢を受け取る部分と、年齢に応じてプレゼントを選ぶ部分を関数にしてみます。

#! /usr/bin/env node

const fs = require('fs')

const getAge = () => {
    console.log('年齢を教えてね')
    const input = parseInt(fs.readFileSync('/dev/stdin', 'utf8'))
    return input
}

const selectGift = (age) => {
    if (age < 0 || isNaN(age)){
        console.log('HOHOHO 正しい年齢を入力してね')
    } else if (age <= 15){
        const presents = JSON.parse(fs.readFileSync('gifts.json', 'utf8'))
        const result = presents.find(present => age === present.age)
        console.log(`メリークリスマス!${result.name}を贈ります${result.emoji}`)
    }
    else {
        const gift = Math.floor(Math.random() * 100) * Math.floor(Math.random() * 100)
        console.log(`メリークリスマス!${gift}円を贈ります🎉`)
    }
}

selectGift(getAge())

標準入力から値を受け取る部分をgetAge関数、プレゼントを選ぶ部分をselectGift関数にしてみました。

ただ各関数には無関係の下位問題がまだありそうです。

たとえばselectGift関数の中では年齢に応じたプレゼントを選ぶ処理と、結果を出力する処理が同時に行われていて読みづらいです。この関数はプレゼントを選ぶ処理に専念させて、出力用の関数を別途作りたいです。

関数をさらに導入

selectGift関数でひとまとめにしていた処理を、下記の3つの関数に書き換えてみました。

  • giftForKids関数: 引数で受け取った年齢に応じてJSONファイルからプレゼント情報を取ってきて返り値に持ちます。
  • giftForAdults関数: Math.random()して出た数2つをかけ合わせ、プレゼントとして渡される金額を取ってきて返り値に持ちます。
  • printOutput関数: 引数で受け取った年齢に応じて出力処理を行います。
#! /usr/bin/env node

const fs = require('fs')

const getAge = () => {
    console.log('年齢を教えてね')
    const input = parseInt(fs.readFileSync('/dev/stdin', 'utf8'))
    return input
}

const giftForKids = (age) => {
    const gifts = JSON.parse(fs.readFileSync('gifts.json', 'utf8'))
    const gift = gifts.find(gift => age === gift.age)
    return gift
}

const giftForAdults = () => {
    const money = Math.floor(Math.random() * 100) * Math.floor(Math.random() * 100)
    return money
}

const printOutput = (age) => {
    if (age < 0 || isNaN(age)) {
        console.log('HOHOHO 正しい年齢を入力してね')
    } else if (age <= 15) {
        console.log(`メリークリスマス!${giftForKids(age).name}を贈ります${giftForKids(age).emoji}`)
    } else {
        console.log(`メリークリスマス!${giftForAdults()}円を贈ります🎉`)
    }
}

printOutput(getAge())

関数に切り分けるとコードがきれいになっているような気持ちになってきました!

ただ、printOutput関数の中のメリークリスマス!${giftForKids(age).name}を贈ります${giftForKids(age).emoji}の箇所がめっちゃ読みづらいのと、fs.readFileSync()が2箇所で重複しているので、共通化できるかも?とも思います。

これでどや

#! /usr/bin/env node

const fs = require('fs')

const getData = (resource) => {
    return fs.readFileSync(resource, 'utf8')
}

const getAge = () => {
    console.log('年齢を教えてね')
    const input = getData('/dev/stdin')
    return parseInt(input)
}

const giftForKids = (age) => {
    const gifts = JSON.parse(getData('gifts.json'))
    const gift = gifts.find(gift => age === gift.age)
    return `メリークリスマス!${gift.name}を贈ります${gift.emoji}`
}

const giftForAdults = () => {
    const money = Math.floor(Math.random() * 100) * Math.floor(Math.random() * 100)
    return `メリークリスマス!${money}円を贈ります🎉`
}

const printOutput = (age) => {
    if (age < 0 || isNaN(age)) {
        console.log('HOHOHO 正しい年齢を入力してね')
    } else if (age <= 15) {
        console.log(giftForKids(age))
    } else {
        console.log(giftForAdults())
    }
}

printOutput(getAge())

う〜ん、getData 関数は名前もよくないし、あんまり必要なさそうな感じがしました。『リーダブルコード』には無理やり必要のない箇所まで関数にすると可読性を損なうと書いてあったので、これはやりすぎなのかもしれないです。

おわりに

プログラムの出来はともかく、自分的にはかなりアグレッシブに無関係の下位問題抽出に取り組めたのでとりあえず満足しました。

JS本当に難しいので、これからも簡単なプログラムを書いて遊びながら慣れていきたいし、いつかはリーダブルなJSを書けるようになれるといいな。

コードの中身で気になるところがある方がいたら教えて下さい🙏

github.com

明日はpofkuma(id:pofkuma)さんです!

参考資料