First Class Collection
配列やコレクションの操作はアプリの様々なところに飛び散りがちです。ショッピングカートのアイテム、RPGのパーティメンバー、労務管理ツールで管理される社員などなど色々なところで操作されるオブジェクトを素朴にそれぞれのコンテキストで操作すると重複コードの原因になります。
ファーストクラスコレクションはそういうケースに高い保守性と安全性をもって対応するためのオブジェクト指向なりの答えです。ちょっと長いですがECサイトで複数のアイテムを入れられるショッピングカートをファーストクラスコレクションで実装した場合の例を見てみます。
class ShoppingCart {
private items: string[];
constructor(items: string[]) {
this.items = items;
}
public addItem(item: string): ShoppingCart {
return new ShoppingCart([...this.items, item]);
}
public removeItem(item: string): ShoppingCart {
const index = this.items.indexOf(item);
if (index !== -1) {
return new ShoppingCart([...this.items.slice(0, index), ...this.items.slice(index + 1)]);
}
return this;
}
public totalCost(): number {
// Calculate the total cost of all items in the shopping cart
let total = 0;
for (const item of this.items) {
total += this.itemCost(item);
}
return total;
}
private itemCost(item: string): number {
// Lookup the cost of the item in a database or API
// For the purposes of this example, we'll just return a fixed cost
return {
'apple': 1,
'orange': 2,
'banana': 3,
}[item] || 0;
}
public getItems(): ReadonlyArray<string> {
return Object.freeze(this.items.slice());
}
}
// Example usage:
const cart = new ShoppingCart(['apple', 'orange']);
const newCart = cart.addItem('banana');
const newerCart = newCart.addItem('peach');
console.log(cart); // ShoppingCart { items: [ 'apple', 'orange' ] }
console.log(newCart); // ShoppingCart { items: [ 'apple', 'orange', 'banana' ] }
console.log(newerCart); // ShoppingCart { items: [ 'apple', 'orange', 'banana', 'peach' ] }
基本的なポイントは2つです。
- コレクション(配列)をprivateなインスタンス変数として持つこと
- コレクション型のインスタンス変数を操作するメソッドをクラス内に実装すること
ただし注意すべき点が2つあります。
副作用を撲滅
🙆♂️ この例は上で示したaddItemメソッドを再掲したものですが、副作用が無い(つまりイミュータブル!!)ように実装してあります。
public addItem(item: string): ShoppingCart {
return new ShoppingCart([...this.items, item]);
}
❌ 副作用ありで実装すると下のようになりますが、これをやってしまうと関数の実行結果がインスタンス変数itemsに依存することになってしまい見通しも悪いですしテストも書きにくいです。
public addItem(item: string): void {
this.items.push(item);
}
外部に値を渡すときはイミュータブルにして渡すこと!
🙆♂️ その時点でカートに入っている全てのアイテムを返すメソッドですが、こんな感じでオブジェクトをイミュータブルにしてから返しましょう。
public getItems(): ReadonlyArray<string> {
return Object.freeze(this.items.slice());
}
❌ もしもこんな感じでそのまま返してしまうと、呼び出し元で勝手にitemsを変更されてしまいます。せっかくitemsをカートクラスの中に閉じ込めたのにその意味がなくなります。
public getItems(): ReadonlyArray<string> {
return this.items;
}