つくる 処理を関数に整理する
このレッスンで分かること
- 1 本の大きな関数を、役割ごとの小さな関数に分けられます
- ガード節、デフォルト引数、
forEachを 1 つのコードで組み合わせられます- 分けた関数を呼び出して組み立てるのが、まとめ役の関数の仕事です
つくる 処理を関数に整理する とは
この章で学んだ関数の書き方を総動員して、プロフィールページの文字列を組み立てる関数を作ります。挨拶と作品一覧を別々の関数に分けるのがポイントです。
ここまでで何ができるようになったか
第6章では、関数まわりの道具を 7 つ手に入れました。振り返っておきましょう。
| レッスン | 手に入れた道具 |
|---|---|
| 関数宣言 | function で処理に名前を付け、引数と戻り値でやり取りする |
| アロー関数 | => で短く書く。使い捨ての処理に向く |
| デフォルト引数 | 引数を省略できるようにする |
| 早期 return | 不正な入力を冒頭ではじき、ネストを浅くする |
| 変数のスコープ | 作業用の変数を関数の中に閉じ込める |
| コールバック関数 | 関数を値として他の関数に渡す |
| forEach | 配列の各要素にコールバックを適用する |
第5章までは、やりたいことを 1 本のコードに上から順に書いていました。第6章を終えたいま、それを役割ごとの部品に分けることができます。今回はその総仕上げとして、山田 太郎さんのプロフィールページに出す文字列を組み立てます。
関数に分けるかどうかの目安は「その処理に名前が付けられるか」です。名前が付けられるなら、それは 1 つの部品です。名前が思い付かないなら、まだ複数の仕事が混ざっています。
何を作るか
作るのは buildProfilePage という関数です。名前、作品の配列、肩書きを受け取り、下記のような 1 本の文字列を返します。
こんにちは、山田 太郎さん
Web エンジニアを目指しています
1. プロフィールサイト
2. 自己紹介カード
3. スキル一覧の表仕様を整理すると下記のとおりです。
- 1 行目は挨拶。
"こんにちは、" + name + "さん" - 2 行目は肩書き。省略されたら
Web エンジニアを目指していますを使う - 3 行目以降は作品の一覧。
1.から始まる連番を付ける - 作品が 1 つも無ければ
作品はまだありませんの 1 行にする - 名前が空、作品が配列でない、といった不正な入力なら
入力エラーだけを返す - 行の区切りは改行
\n
分け方を先に決める
いきなり書き始めず、どんな部品に分けるかを先に決めます。仕様を読み直すと、性質の違う仕事が 3 つ混ざっています。
| 仕事 | 担当する関数 |
|---|---|
| 挨拶の 1 行を作る | greet(name) |
| 作品の一覧を組み立てる | listWorks(works) |
| 入力を検査して全体を組み立てる | buildProfilePage(...) |
この分け方には理由があります。挨拶の文言を変えたくなったら greet だけを見ればよく、作品の並べ方を変えたくなったら listWorks だけを見ればよい。修正の影響が閉じるからです。まとめ役の buildProfilePage は、部品を呼んでつなぐことに集中します。
部品に分ける最大の利点は、直す場所が 1 か所に決まることです。「どこを直せばよいか分からない」という状態は、たいてい 1 本の関数に仕事を詰め込みすぎたときに起こります。
部品 1 挨拶を作る
一番小さい部品から書きます。
function greet(name) {
return "こんにちは、" + name + "さん";
}引数を受け取って文字列を返すだけです。ここに入力の検査は書きません。検査はまとめ役の仕事だと決めたからです。1 つの関数に 1 つの責任、を守ります。
部品 2 作品の一覧を組み立てる
配列を forEach でまわし、行の配列を作ってから join でつなぎます。
function listWorks(works) {
const lines = [];
works.forEach((work, index) => {
lines.push((index + 1) + ". " + work);
});
if (lines.length === 0) return "作品はまだありません";
return lines.join("\n");
}lines は関数の中で宣言したローカル変数です。スコープのレッスンで学んだとおり、作業用の変数は必ず関数の中に閉じ込めます。関数の外に置いてしまうと、2 回目に呼んだときに前回の行が残ります。
forEach に渡しているアロー関数がコールバックです。第 2 引数の index から連番を作っています。
部品 3 まとめ役
最後に全体を組み立てます。冒頭はガード節です。
function buildProfilePage(name, works, role = "Web エンジニアを目指しています") {
if (typeof name !== "string" || name === "") return "入力エラー";
if (!Array.isArray(works)) return "入力エラー";
return greet(name) + "\n" + role + "\n" + listWorks(works);
}読んでみてください。最初の 2 行が「受け付けない入力」、最後の 1 行が「やりたいこと」です。本体はたった 1 行で、しかも一番浅いインデントにあります。この形にたどり着けるのが、第6章を学んだ成果です。
role にはデフォルト引数を使っています。肩書きを渡さない呼び出しがほとんどなので、既定値を持たせておけば呼び出し側が短く書けます。
Array.isArray(works) は、値が配列かどうかを調べる書き方です。typeof では配列も "object" になってしまうため、配列の判定にはこちらを使います。
完成したコードを上から読んでみて、「何をしているか」が関数名だけで追えるなら分割は成功です。中身を読まないと分からないなら、名前か分け方を見直します。
動きを追う
buildProfilePage("山田 太郎", ["プロフィールサイト", "自己紹介カード"]) を呼んだときの流れは下記のとおりです。
nameは空でない文字列なので、1 つ目のガードを通過するworksは配列なので、2 つ目のガードも通過するroleは渡されていないので既定値が使われるgreet("山田 太郎")が"こんにちは、山田 太郎さん"を返すlistWorks([...])が 2 行の文字列を返す- 3 つを
\nでつないで返す
分けすぎにも気を付ける
分割は万能ではありません。1 行しかない処理を関数にすると、かえって読みにくくなります。
// 分けすぎの例 定義を探しに行く手間のほうが大きい
function addSuffix(name) {
return name + "さん";
}
function addPrefix(text) {
return "こんにちは、" + text;
}
function greet(name) {
return addPrefix(addSuffix(name));
}greet を読むのに 3 か所を行き来することになりました。分ける判断の目安は下記のとおりです。
| 状況 | 分けるべきか |
|---|---|
| 同じ処理が 2 か所以上に出てくる | 分ける |
| 処理に名前が付けられて数行ある | 分ける |
| 中身が 1 行で 1 か所でしか使わない | 分けない |
| テストしたい単位がある | 分ける |
今回の greet と listWorks は、どちらも「名前が付けられて、独立して確かめられる」単位です。だから分ける価値があります。
よくある間違い
1. 部品の中でも入力検査をしてしまう
// 検査があちこちに散らばると、どこが本当の入口か分からなくなる
function greet(name) {
if (!name) return "入力エラー";
return "こんにちは、" + name + "さん";
}検査は入口の 1 か所にまとめます。部品は正常な値が来る前提で書いてよい、という約束にしておくと全体が読みやすくなります。
2. 作業用の配列を関数の外に置く
// NG 2 回目の呼び出しで前回の行が残る
const lines = [];
function listWorks(works) {
works.forEach((work, index) => {
lines.push((index + 1) + ". " + work);
});
return lines.join("\n");
}スコープのレッスンで見たグローバル汚染そのものです。lines は listWorks の中で宣言します。
3. 空配列のときの分岐を忘れる
// works が空だと空文字列が返り、挨拶の下に何も無い行ができる
return lines.join("\n");[].join("\n") は空文字列です。エラーにはならないので気付きにくく、画面には不自然な余白だけが残ります。空のときに何を返すかは、仕様として先に決めておきましょう。
要件
- 関数名は buildProfilePage、引数は name, works, role の順にすること
- role にはデフォルト引数
Web エンジニアを目指していますを設定すること - 不正な入力はガード節で冒頭にはじき、文字列
入力エラーを返すこと - 挨拶を作る関数と作品一覧を作る関数に分け、buildProfilePage から呼び出すこと
入出力例
buildProfilePage("山田 太郎", ["プロフィールサイト","自己紹介カード","スキル一覧の表"]) → "こんにちは、山田 太郎さん
Web エンジニアを目指しています
1. プロフィールサイト
2. 自己紹介カード
3. スキル一覧の表"
buildProfilePage("鈴木 花子", ["自己紹介カード"], "デザイナー") → "こんにちは、鈴木 花子さん
デザイナー
1. 自己紹介カード"
buildProfilePage("山田 太郎", []) → "こんにちは、山田 太郎さん
Web エンジニアを目指しています
作品はまだありません"
buildProfilePage("", ["プロフィールサイト"]) → "入力エラー"
buildProfilePage("山田 太郎", null) → "入力エラー"