JavaScript | 配列・オブジェクト:パフォーマンス・設計 – 関数分割

JavaScript JavaScript
スポンサーリンク

関数分割とは何か

関数分割は「大きな処理を、明確な役割ごとの“小さな関数”に切り出して組み合わせる」設計です。ここが重要です:1つの関数は1つの責務(Single Responsibility)。入力と出力をはっきり決め、内部状態に依存しない“純粋な関数”を増やすと、読みやすさ・テスト容易性・性能最適化が同時に手に入ります。

// 大きな一体関数(読みにくい)
function process(items, cfg) {
  const normalized = items.map(x => ({ ...x, name: x.name.trim().toLowerCase() }));
  const filtered = normalized.filter(x => !cfg.enabledOnly || x.active);
  const sorted = filtered.toSorted((a,b)=>a.price-b.price);
  return sorted.slice((cfg.page-1)*cfg.perPage, cfg.page*cfg.perPage);
}

// 分割版(意図が伝わる)
const normalize = x => ({ ...x, name: x.name.trim().toLowerCase() });
const filterBy   = (x, cfg) => (!cfg.enabledOnly || x.active);
const sortBy     = (a, b) => a.price - b.price;
const paginate   = (list, page, per) => list.slice((page-1)*per, (page-1)*per + per);

function processFast(items, cfg) {
  const normalized = items.map(normalize);
  const filtered   = normalized.filter(x => filterBy(x, cfg));
  const sorted     = filtered.toSorted(sortBy);
  return paginate(sorted, cfg.page, cfg.perPage);
}
JavaScript

分割の基本方針(責務、入出力、純粋性)

責務を1つに絞る

関数は「名前通りの1つの仕事」だけをします。フィルタはフィルタ、変換は変換、集計は集計。余計な副作用(ログ、状態書き換え)を混ぜないことで、再利用性が上がり、結合が緩くなります。

const normalizeName = s => s.trim().toLowerCase();
const toViewRow = x => ({ id: x.id, name: normalizeName(x.name), price: x.price });
JavaScript

入出力の契約を明確に

引数の型と返り値の型を決め、欠損の扱い(null/undefined)や既定値を統一します。契約が明確だと、呼び出し側の防御コードが減り、パイプラインが真っ直ぐになります。

function toPrice(s, def = null) {
  const n = Number.parseFloat(String(s));
  return Number.isFinite(n) ? n : def; // 失敗時は def を返す契約
}
JavaScript

純粋な関数を基本に

同じ入力は同じ出力、外部状態を書き換えない。純粋性が高いほどテストが容易になり、並列化やメモ化の恩恵を受けやすくなります。

// 純粋:入力をコピーして新値を返す
const setActive = x => ({ ...x, active: true });
JavaScript

実務での分割例(配列・オブジェクトの定番処理)

フィルタ、変換、ソート、ページングの分離

表示用の典型処理は“段ごと関数”に分けると、差し替えやテストが容易です。フィルタ条件やソートキーを入れ替えるだけで使い回せます。

const whereEnabled = f => x => (!f.enabledOnly || x.active);
const whereQuery   = f => {
  const q = f.q?.trim().toLowerCase();
  return !q ? () => true : x => x.name.toLowerCase().includes(q);
};
const cmpPriceAsc  = (a, b) => a.price - b.price;

function querySortPage(items, f) {
  const pred = x => whereEnabled(f)(x) && whereQuery(f)(x);
  const filtered = items.filter(pred);
  const sorted   = filtered.toSorted(cmpPriceAsc);
  return paginate(sorted, f.page ?? 1, f.perPage ?? 20);
}
JavaScript

groupBy、辞書化、統計の分割

“形を作る”関数と“値を計算する”関数を分ける。構造とロジックが独立し、変更コストが下がります。

const groupBy = (list, keyFn) =>
  list.reduce((acc, x) => ((acc[keyFn(x)] ??= []).push(x), acc), {});

const toDict = (list, key = "id") =>
  Object.fromEntries(list.map(x => [String(x[key]), x]));

const statsPrice = list =>
  list.reduce((a, x) => ({ count: a.count+1, sum: a.sum + (x.price ?? 0) }), { count: 0, sum: 0 });
JavaScript

可読性と速度の両立(分割しつつ最適化)

宣言と実装の二層化

外からは分かりやすい“宣言的”関数名で並べ、内部では1パス融合や前計算で高速化します。意図を壊さずに最適化できるのが利点です。

function applyFilters(items, f) {
  const out = [];
  const q = f.q?.trim().toLowerCase();
  for (const x of items) {
    if (f.enabledOnly && !x.active) continue;
    if (q && !x.name.toLowerCase().includes(q)) continue;
    out.push(x);
  }
  return out;
}

function pipeline(items, f) {
  const filtered = applyFilters(items, f);     // 読みやすい名前
  const sorted   = filtered.toSorted(cmpPriceAsc);
  return paginate(sorted, f.page, f.perPage);
}
JavaScript

前計算キーで重い処理を外へ出す

正規化や日付変換を先に1回だけ行い、後段の関数は軽いキーを使って速く動かす。分割の恩恵を保ったまま性能が出ます。

const prepare = x => ({ x, nameKey: x.name.trim().toLowerCase(), t: new Date(x.createdAt).getTime() });

function filterPrepared(prepared, q, cutoff) {
  const keyQ = q?.trim().toLowerCase();
  return prepared.filter(p => (!keyQ || p.nameKey.includes(keyQ)) && p.t >= cutoff);
}
JavaScript

分割の落とし穴と回避(重要ポイントの深掘り)

過剰分割で意図が見えなくなる

小さくしすぎると“つながり”が見えず、読みにくい。1関数が“意味のあるひとかたまり”になるよう、粒度を意識します。目安は10〜30行程度、引数は3つ以内に収めると扱いやすいことが多いです。

引数の乱立とクロージャ依存

引数が増えすぎると誤用が増えます。設定は“パラメータオブジェクト”でまとめるか、部分適用で閉じ込めると安全です。ただし大きなオブジェクトをクロージャにキャプチャしすぎるとメモリが膨らむため、必要最小限に留めます。

const makeWhere = f => x => (!f.enabledOnly || x.active) && (!f.q || x.name.includes(f.q));
JavaScript

純粋性を壊す副作用

ログ、外部書き換え、破壊的操作が混ざると、再利用・テストが難しくなります。副作用は末端(入出力層)へ寄せ、ドメインロジックは純粋に保ちます。


テスト容易性とエラーハンドリング(分割の実務メリット)

小さなユニットテストで品質を担保

フィルタ条件、変換、比較関数などは入力と期待出力を固定しやすく、テストが極めて簡単です。大きな一体関数より、バグの局所化ができます。

// 例:比較関数のテスト
expect(cmpPriceAsc({price:1},{price:2})).toBeLessThan(0);
expect(cmpPriceAsc({price:2},{price:1})).toBeGreaterThan(0);
JavaScript

失敗しやすい箇所を独立させる

数値変換、日付パース、正規化は失敗が起きやすい。変換関数を分離して、失敗時の既定値やエラーコードを統一すると、呼び出し側をシンプルに保てます。

function toDateMs(s) {
  const t = new Date(s).getTime();
  return Number.isNaN(t) ? null : t;
}
JavaScript

すぐ使える分割レシピ(現場の最短コード)

パイプを関数で表現

const normalize = x => ({ ...x, name: x.name.trim().toLowerCase() });
const where     = f => x => (!f.enabledOnly || x.active) && (!f.q || x.name.toLowerCase().includes(f.q.toLowerCase()));
const sortBy    = (a, b) => a.price - b.price;

function transform(items, f) {
  return paginate(
    items.map(normalize).filter(where(f)).toSorted(sortBy),
    f.page ?? 1, f.perPage ?? 20
  );
}
JavaScript

1パス融合版(大規模向け)

function transformFast(items, f) {
  const out = [];
  const q = f.q?.trim().toLowerCase();
  for (const it of items) {
    const name = it.name.trim().toLowerCase();
    if (f.enabledOnly && !it.active) continue;
    if (q && !name.includes(q)) continue;
    out.push({ id: it.id, name, price: it.price });
  }
  const sorted = out.toSorted((a,b)=>a.price-b.price);
  return paginate(sorted, f.page ?? 1, f.perPage ?? 20);
}
JavaScript

まとめ

関数分割の核心は「1関数=1責務」「入出力の契約を明確に」「純粋性を保つ」です。フィルタ・変換・ソート・ページングを段ごとに分け、宣言的に並べると意図が伝わり、差し替えやテストが容易になります。性能が必要な箇所は内部で1パス融合や前計算キーを使い、可読性を崩さず高速化する“二層設計”を選ぶ。過剰分割や副作用混入を避け、失敗しやすい変換は独立させる。これを徹底すれば、初心者でも読みやすくて速い配列・オブジェクト処理を、安定した設計で組み立てられます。

タイトルとURLをコピーしました