「責務分離」を非同期コードで考える意味
非同期処理って、それだけで頭を使いますよね。
そこに「UI」「エラー処理」「キャッシュ」「ローディング表示」などを全部一緒に書き始めると、一瞬でカオスになります。
だからこそ大事になるのが 責務分離(責任の分け方) です。
ざっくり言うと、
「一つの関数・一つのモジュールに、役割を詰め込みすぎない」
「“誰が何を担当するか”を、はっきり分けてあげる」
という考え方です。
非同期コードで責務分離ができていると、
処理の流れが追いやすい
テストしやすい
変更しやすい
バグの原因を見つけやすい
という、かなり大きなメリットが生まれます。
まずは「ごちゃ混ぜな悪い例」を見てみる
何でもかんでも 1 関数に詰め込んだコード
次のようなコードを想像してみてください。
async function initPage() {
// API 呼び出し
const res = await fetch("/api/user");
if (!res.ok) {
alert("ユーザー取得に失敗しました");
return;
}
const user = await res.json();
// キャッシュに保存
window.__userCache = user;
// UI 更新
const header = document.querySelector("#header");
header.textContent = `こんにちは、${user.name}さん`;
// ローディング非表示
document.querySelector("#loading").style.display = "none";
}
JavaScriptこの関数、やっていることを分解すると、
API 呼び出し
エラー処理(アラート表示)
キャッシュへの保存
DOM 操作(UI 更新)
ローディング表示の制御
が全部入りです。
動くことは動きますが、
テストしづらい
再利用しづらい
UI を変えたいだけなのに API 部分まで触ることになる
エラー処理の方針を変えたいだけなのに DOM も巻き込まれる
という「絡まり」が起きます。
ここから、「責務分離」の出番です。
責務を分ける軸を決める
よく使う分け方の例
非同期コードでよく使う分け方は、例えばこんな感じです。
データ取得の責務(API 呼び出し・非同期処理)
状態管理・キャッシュの責務
UI 更新の責務
エラー表示・ローディング表示の責務
全部を一気に完璧に分ける必要はありません。
最初は「せめてこれは分けよう」という軸を一つ決めるだけで、だいぶ読みやすくなります。
ここでは、さっきの例を「段階的に」分けていきます。
ステップ1:データ取得(非同期処理)を外に出す
「API を叩いてユーザーを取ってくる」だけの関数にする
まずは、API 呼び出し部分を専用の関数に切り出します。
async function fetchCurrentUser() {
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error("ユーザー取得に失敗しました");
}
return res.json();
}
JavaScriptこの関数の責務は一つだけです。
「ユーザーを取得し、失敗したら例外を投げる」
UI もキャッシュも知りません。
ただ「データを取る」ことだけを担当します。
これを使って initPage を書き直すと、こうなります。
async function initPage() {
try {
const user = await fetchCurrentUser();
// ここから先で UI やキャッシュを扱う
} catch (err) {
alert(err.message);
}
}
JavaScriptこれだけでも、
API 呼び出しの詳細
エラーの判定方法
が initPage から消え、
「ページ初期化の流れ」が少し見えやすくなりました。
ステップ2:UI 更新の責務を分ける
「ユーザーを受け取って UI を更新する」関数にする
次に、UI 更新部分を関数に切り出します。
function renderUserHeader(user) {
const header = document.querySelector("#header");
header.textContent = `こんにちは、${user.name}さん`;
}
JavaScriptローディング表示の制御も分けてしまいます。
function hideLoading() {
document.querySelector("#loading").style.display = "none";
}
JavaScriptこれを使って initPage を書き直すと、こうなります。
async function initPage() {
try {
const user = await fetchCurrentUser();
window.__userCache = user;
renderUserHeader(user);
hideLoading();
} catch (err) {
alert(err.message);
}
}
JavaScriptここまで来ると、initPage はだいぶ「物語」として読めるようになります。
ユーザーを取得する
キャッシュに保存する
ヘッダーを描画する
ローディングを消す
という流れが、上から下に素直に追えます。
ステップ3:キャッシュと UI をさらに分離する
キャッシュの責務を関数に閉じ込める
グローバル変数に直接触るのではなく、
キャッシュ用の関数を用意します。
const userCache = {
current: null,
};
function setCurrentUser(user) {
userCache.current = user;
}
function getCurrentUser() {
return userCache.current;
}
JavaScriptこれで、
「キャッシュの持ち方」を変えたくなったときも、
このモジュールだけを触れば済みます。
initPage はこうなります。
async function initPage() {
try {
const user = await fetchCurrentUser();
setCurrentUser(user);
renderUserHeader(user);
hideLoading();
} catch (err) {
alert(err.message);
}
}
JavaScript責務の分かれ方は、こうです。
fetchCurrentUser:データ取得setCurrentUser / getCurrentUser:キャッシュrenderUserHeader / hideLoading:UIinitPage:それらを組み合わせて「ページ初期化」という物語を作る
ここが重要です。
「一つの関数が“何を知っているか”を減らしていく。
API の詳細も、DOM の詳細も、キャッシュの詳細も、全部知っている関数を作らない。」
非同期コードで責務分離が効いてくる瞬間
仕様変更が来たときの「痛み」が変わる
例えば、こういう変更が来たとします。
ユーザー取得 API の URL が変わった
エラー表示を alert ではなく画面上のエラーバナーにしたい
ローディング表示をスケルトン UI に変えたい
キャッシュを sessionStorage に保存したい
責務分離されていない最初のコードだと、
全部 initPage を触ることになります。
しかも、変更のたびに「他の部分を壊していないか」を毎回不安に思うことになります。
責務分離されているコードだと、
API の変更 → fetchCurrentUser だけ
エラー表示の変更 → initPage の catch 部分だけ
ローディングの変更 → hideLoading だけ
キャッシュの変更 → setCurrentUser / getCurrentUser だけ
というふうに、
「どこを触ればいいか」が明確です。
ここが重要です。
「責務分離は、“今読むため”だけじゃなく、“未来に変えるため”の投資でもある。」
非同期処理ならではの責務分離のポイント
「非同期そのもの」と「非同期の結果をどう使うか」を分ける
非同期コードで特に意識してほしいのは、
非同期処理を「実行する責務」
非同期処理の「結果をどう扱うかの責務」
を分けることです。
例えば、
fetchCurrentUser は「実行する側」initPage は「結果を受け取って UI に反映する側」
という分け方です。
これを意識すると、
テストでは fetchCurrentUser をモックして、initPage の UI 更新だけ確認する
別の画面でも fetchCurrentUser を再利用する
といったことがやりやすくなります。
「エラーを投げる側」と「エラーを見せる側」を分ける
もう一つ大事なのが、
エラー処理の責務分離です。
fetchCurrentUser は「失敗したら例外を投げる」initPage は「例外を受け取って、ユーザーにどう見せるかを決める」
という分け方にしておくと、
API のエラー判定ロジックを変えたいときは fetch 側だけ
エラーの見せ方(ダイアログ・トースト・画面上のバナー)を変えたいときは UI 側だけ
を触ればよくなります。
非同期処理は失敗がつきものなので、
「どこまでが“技術的な失敗”で、どこからが“ユーザーに見せる表現”か」
を分けておくと、設計が一気にきれいになります。
初心者として「責務分離」で意識してほしい質問
最後に、あなたに持っていてほしい問いをまとめます。
この関数は、一言で何をする関数と言えるか?
この関数は、API も UI もキャッシュも、全部知りすぎていないか?
非同期処理を「実行する責務」と「結果を使う責務」がごちゃ混ぜになっていないか?
エラーを「検出する場所」と「ユーザーに見せる場所」が分かれているか?
仕様変更が来たときに、「どこを触ればいいか」がすぐに分かる構造になっているか?
おすすめの練習は、
自分の非同期関数を一つ選んで、こう自問してみることです。
「この関数から、“知らなくていいこと”をどれだけ追い出せるだろう?」
そこから、
データ取得を外に出す
UI 更新を外に出す
キャッシュを外に出す
エラー表示を外に出す
と少しずつ分けていくと、
コードがスッと呼吸し始めます。
その感覚を一度味わうと、
あなたはもう「とりあえず async/await を書く人」ではなく、
「非同期処理の責務を設計できるエンジニア」 に足を踏み入れています。
