つくる 処理を関数に整理する

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 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("山田 太郎", ["プロフィールサイト", "自己紹介カード"]) を呼んだときの流れは下記のとおりです。

  1. name は空でない文字列なので、1 つ目のガードを通過する
  2. works は配列なので、2 つ目のガードも通過する
  3. role は渡されていないので既定値が使われる
  4. greet("山田 太郎")"こんにちは、山田 太郎さん" を返す
  5. listWorks([...]) が 2 行の文字列を返す
  6. 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 か所でしか使わない分けない
テストしたい単位がある分ける

今回の greetlistWorks は、どちらも「名前が付けられて、独立して確かめられる」単位です。だから分ける価値があります。

よくある間違い

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"); }

スコープのレッスンで見たグローバル汚染そのものです。lineslistWorks の中で宣言します。

3. 空配列のときの分岐を忘れる

// works が空だと空文字列が返り、挨拶の下に何も無い行ができる return lines.join("\n");

[].join("\n") は空文字列です。エラーにはならないので気付きにくく、画面には不自然な余白だけが残ります。空のときに何を返すかは、仕様として先に決めておきましょう。

要件

  1. 関数名は buildProfilePage、引数は name, works, role の順にすること
  2. role にはデフォルト引数 Web エンジニアを目指しています を設定すること
  3. 不正な入力はガード節で冒頭にはじき、文字列 入力エラー を返すこと
  4. 挨拶を作る関数と作品一覧を作る関数に分け、buildProfilePage から呼び出すこと

入出力例

buildProfilePage("山田 太郎", ["プロフィールサイト","自己紹介カード","スキル一覧の表"]) → "こんにちは、山田 太郎さん Web エンジニアを目指しています 1. プロフィールサイト 2. 自己紹介カード 3. スキル一覧の表" buildProfilePage("鈴木 花子", ["自己紹介カード"], "デザイナー") → "こんにちは、鈴木 花子さん デザイナー 1. 自己紹介カード" buildProfilePage("山田 太郎", []) → "こんにちは、山田 太郎さん Web エンジニアを目指しています 作品はまだありません" buildProfilePage("", ["プロフィールサイト"])"入力エラー" buildProfilePage("山田 太郎", null)"入力エラー"

ヒント

main.js
main.js
学習モード

メモ

つくる 処理を関数に整理する

⌘S で保存