つくる 作品コレクションをクラスにする
このレッスンで分かること
- 作品 1 件の
Workと、その集まりのWorkCollectionに役割を分けられる- 配列そのものを
#で隠すと、外からpushやspliceで壊されなくなるstaticの窓口から作り、集計と並び替えをコレクション側のメソッドにまとめられる
つくる 作品コレクションをクラスにする とは
この章で学んだ
class#extendsstaticを全部使って、作品の一覧をひとつのクラスにまとめます。ポートフォリオアプリのデータの中心になる部分です。
ここまでで何ができるようになったか
第5章では、4 つの道具を手に入れました。
| レッスン | 手に入れたもの | この回での使い道 |
|---|---|---|
| クラスとインスタンス | class と constructor | 作品 1 件を Work にする |
| private フィールドとアクセサ | # と get | 表示回数と作品の配列を守る |
| 継承と super | extends と super | 集計の土台を親に置く |
| 多態と static | static | 記録の配列からコレクションを作る窓口 |
入門の最後では、作品の配列がグローバルに 1 本置いてあって、描画も絞り込みも集計もその配列を直接見ていました。第2章で map と filter を、第3章で関数を道具にする書き方を学んだので、処理は関数に分かれています。それでも「作品の配列」だけは誰でも触れる場所に置かれたままです。
この回で、そこに蓋をします。
2 つのクラスに分ける
役割を分けます。Work は作品 1 件のことだけを知り、WorkCollection は「複数あること」だけを知ります。
class Work {
#views;
constructor({ id, title, year, views, published }) {
this.id = id;
this.title = title;
this.year = year;
this.#views = views;
this.published = published;
}
get views() {
return this.#views;
}
get line() {
return `${this.title} (${this.year}) ${this.#views} 回`;
}
}constructor の引数で分割代入を使っています。第4章でやった書き方です。呼ぶ側は new Work(record) と渡すだけで済み、引数の順番を覚える必要がありません。
配列を隠す
コレクションの側では、作品の配列を #works にします。
class WorkCollection {
#works = [];
add(record) {
this.#works.push(new Work(record));
return this;
}
get size() {
return this.#works.length;
}
published() {
return this.#works.filter((work) => work.published);
}
totalViews() {
return this.published().reduce((sum, work) => sum + work.views, 0);
}
topTitles(limit) {
return this.published()
.sort((a, b) => b.views - a.views)
.slice(0, limit)
.map((work) => work.title);
}
static fromRecords(records) {
const collection = new WorkCollection();
records.forEach((record) => collection.add(record));
return collection;
}
}#works が外から見えないので、collection.works.push({}) のような書き方はできません。作品を足す道は add だけです。add の中で new Work(record) を通るので、コレクションに入っているものは必ず Work のインスタンスだと言い切れます。
sortは元の配列を並び替えてしまいます。ここで安全なのは、published()がfilterの戻り値、つまり毎回作り直された別の配列を返しているからです。this.#works.sort(...)と直接書くと、隠したはずの配列が並び替わって元の順番が失われます。
add が this を返しているので、collection.add(a).add(b) とつなげて書けます。
static を入口にする
fromRecords はインスタンスを作る前に呼びたいので static です。new WorkCollection() を書いてから forEach で足す手順が、呼ぶ側から消えます。
const collection = WorkCollection.fromRecords(records);
console.log(collection.size); // 5
console.log(collection.totalViews()); // 275
console.log(collection.topTitles(2)); // ["プロフィールサイト", "作品ギャラリー"]画面に渡す形を 1 回で作る
第8章から先では、この結果を DOM に流し込みます。そのとき何度もメソッドを呼び分けるより、画面に必要な値をひとまとめのオブジェクトで返しておくほうが扱いやすくなります。
function buildCollectionView(records, limit) {
const collection = WorkCollection.fromRecords(records);
return {
size: collection.size,
publishedCount: collection.published().length,
totalViews: collection.totalViews(),
topTitles: collection.topTitles(limit),
lines: collection.published().map((work) => work.line)
};
}クラスの外に出すのは、この関数のような素のオブジェクトにしておくと後が楽です。画面を描く側がクラスの中身を知らずに済むので、あとで
Workの作りを変えても描画のコードを直さずに済みます。
この先どうつながるか
WorkCollection は、この章のゴールであると同時に、残りの章の土台になります。
| 章 | ここに足すもの |
|---|---|
| 第6章 | 記録の形が壊れていたときに投げるエラー |
| 第7章 | タイトルや年の文字を正規表現で検査する処理 |
| 第9章 | API から取った配列を fromRecords に渡す口 |
どの章でも、直すのは WorkCollection の中だけで済みます。画面を描く側は buildCollectionView が返すオブジェクトだけを見ているからです。境目を 1 か所に絞ったので、変更の影響が外に漏れません。
入門の最後の
script.jsでは、作品の配列を書き換える処理が 3 か所に散っていました。同じ機能でも、触れる場所を減らすと壊れにくくなります。クラスを使う目的はそこにあります。
よくある間違い
#works を返してしまう
get works() { return this.#works; } と書くと、隠した意味がありません。返した配列は同じ入れ物なので、外から push も splice も通ります。渡すなら [...this.#works] のようにコピーを返します。
const list = collection.works;
list.push({ title: "偽の作品" }); // 中身が増えてしまうtotalViews を全件で計算する
this.#works.reduce(...) と書くと、非公開の作品の表示回数まで合計に入ります。合計と件数で、数える対象を揃えます。
limit に何も渡さない
topTitles() のように引数なしで呼ぶと slice(0, undefined) になり、全件が返ります。0 件のつもりで呼ぶと全部出てくるので、呼ぶ側で件数を決めて必ず渡します。
要件
- class Work は constructor で { id, title, year, views, published } を分割代入で受け取る。views は #views に入れ、get views() と get line() を用意する。line は
タイトル (年) N 回の形 - class WorkCollection は #works を空の配列で持つ。add(record) は Work を作って足し this を返す。get size() は件数を返す
- published() は published が true の Work だけの配列を返す。totalViews() はその views の合計。topTitles(limit) はその中を views の多い順に並べた先頭 limit 件のタイトルの配列
- static fromRecords(records) は WorkCollection を作って records を全部 add してから返す
- function buildCollectionView(records, limit) を定義し、{ size, publishedCount, totalViews, topTitles, lines } を返す。lines は published() の line を元の順番のまま並べた配列
入出力例
buildCollectionView([{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026},{"id":3,"published":true,"title":"スキル一覧の表","views":32,"year":2026},{"id":4,"published":true,"title":"作品ギャラリー","views":75,"year":2026},{"id":5,"published":false,"title":"学習ログ","views":0,"year":2026}], 2) → {"lines":["プロフィールサイト (2026) 120 回","自己紹介カード (2026) 48 回","スキル一覧の表 (2026) 32 回","作品ギャラリー (2026) 75 回"],"publishedCount":4,"size":5,"topTitles":["プロフィールサイト","作品ギャラリー"],"totalViews":275}
buildCollectionView([], 3) → {"lines":[],"publishedCount":0,"size":0,"topTitles":[],"totalViews":0}
buildCollectionView([{"id":5,"published":false,"title":"学習ログ","views":0,"year":2026}], 1) → {"lines":[],"publishedCount":0,"size":1,"topTitles":[],"totalViews":0}
buildCollectionView([{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026}], 0) → {"lines":["プロフィールサイト (2026) 120 回","自己紹介カード (2026) 48 回"],"publishedCount":2,"size":2,"topTitles":[],"totalViews":168}
buildCollectionView([{"id":2,"published":true,"title":"自己紹介カード","views":48,"year":2026},{"id":1,"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"id":3,"published":true,"title":"スキル一覧の表","views":32,"year":2026}], 10) → {"lines":["自己紹介カード (2026) 48 回","プロフィールサイト (2026) 120 回","スキル一覧の表 (2026) 32 回"],"publishedCount":3,"size":3,"topTitles":["プロフィールサイト","自己紹介カード","スキル一覧の表"],"totalViews":200}