【JavaScript】groupBy をカスタム実装した話
こんにちは!
ラブグラフエンジニアのひろです。
多くのプログラミング言語では、データを特定のキーでグループ化する groupBy
関数が標準で提供されています。
しかし、 JavaScript にはそのような機能が標準で組み込まれていないため、他言語から移行する際に不便を感じることがあります。
この記事では、 JavaScript で groupBy
関数をカスタム実装する一例を紹介します。
リファクタリングの動機と目的
当社では groupBy
を以下のような形で用意していました。
export default f => xs => xs.reduce((acc, x) => Object.assign({}, acc, { [f(x)]: [...acc[f(x)] || [], x] }), {});
動作に問題はなかったものの、ワンライナーで書かれており、可読性が低下していました。
この問題を解決するため、より読みやすい groupBy
を求めて、リファクタリングをおこなうことにしました。
groupBy
メソッドの実装
新しい 新しい groupBy
メソッドでも、元の実装と同じく reduce
関数を使用してデータをキーに基づいてグループ化します。
完成したコードがこちらです。
static groupBy(objectsArray, key) {
return objectsArray.reduce((result, currentObject) => {
// グループ化のためのキーを取得
const keyName = currentObject[key];
// キーが存在しない場合は、空のオブジェクトを返す
if (!keyName) {
return result;
}
// まだそのキーのグループが存在しない場合は、新たに空の配列を作成
if (!result[keyName]) {
result[keyName] = [];
}
// 現在のアイテムをグループに追加
result[keyName].push(currentObject);
return result;
}, {}); // 初期値は空のオブジェクト
}
このコードでは、配列の各要素を取り出し、指定されたキーに基づいて値を分類しています。
各キーの値ごとに新しい配列を作成し、該当するオブジェクトを配列に追加していきます。
groupBy
メソッドの実用例
実装した 例えば、以下のようなオブジェクトの配列があるとします。
const people = [
{ name: "二郎", age: 25 },
{ name: "太朗", age: 30 },
{ name: "花子", age: 25 }
];
const peopleGroupedByAge = groupBy(people, 'age');
console.log(peopleGroupedByAge);
出力結果は次のようになります。
{
"25": [{ name: "二郎", age: 25 }, { name: "花子", age: 25 }],
"30": [{ name: "太朗", age: 30 }]
}
このように、任意の属性でオブジェクトを効果的にグループ化できるようになっています。
パフォーマンスの評価
最初のワンライナーのコードも、改善後のコードも、どちらも返り値は同じで引数もほぼ同じものですが、リファクタリングにより、コードの可読性が大幅に向上しました。
ではパフォーマンスについてはどうでしょうか?
以下のようなコードを用意して計測してみました。
class Utils {
static oldGroupBy = (xs, f) =>
xs.reduce((acc, x) =>
Object.assign({}, acc, { [f(x)]: [...acc[f(x)] || [], x] }),
{});
static newGroupBy(objectsArray, key) {
return objectsArray.reduce((result, currentObject) => {
const keyName = currentObject[key];
if (!keyName) {
return result;
}
if (!result[keyName]) {
result[keyName] = [];
}
result[keyName].push(currentObject);
return result;
}, {});
}
}
// テスト用データを作成
const largeDataSet = [];
for (let i = 0; i < 10000; i++) {
largeDataSet.push({ name: `Person${i}`, age: Math.floor(Math.random() * 100) });
}
// oldGroupBy の実行時間を計測
console.time("oldGroupBy");
const oldPeopleGroupedByAge = Utils.oldGroupBy(largeDataSet, x => x.age);
console.timeEnd("oldGroupBy");
// newGroupBy の実行時間を計測
console.time("newGroupBy");
const newPeopleGroupedByAge = Utils.newGroupBy(largeDataSet, 'age');
console.timeEnd("newGroupBy");
結果は以下の通り。
回数 | oldGroupBy | newGroupBy |
---|---|---|
1回目 | 254.275ms | 1.676ms |
2回目 | 250.351ms | 1.725ms |
3回目 | 238.392ms | 1.573ms |
4回目 | 250.11ms | 1.623ms |
5回目 | 243.045ms | 1.61ms |
約150倍の速度ということで、テストデータの形式においてはかなりの改善が見られました。
reduce を使っている点は同じなので、 oldGroupBy で Object.assign
によって新しいオブジェクトを生成している部分でここまでの差が生まれたのではないかと考えています。
まとめ
この記事では、 JavaScript における groupBy
メソッドの実装例を紹介しました。
何事もバランスが大事ではありますが、行数が増えたとしても、読みやすいコードにする意義はあると思うので、これからも意識していこうと思っています。
groupBy
の実装に役立つ reduce
関数の詳細については、こちらの記事で詳しく解説していますので、ぜひご覧ください。
Discussion