プロジェクトなりサービスなりが成長すると取り扱う対象が増えていきます。この変化につれてデータ構造を変える必要も出てきやすいです。このデータ構造の変化の際に後方互換性を保ちやすくする小技を一つ紹介します。
ここでいうデータ構造の後方互換性とは新しいバージョンのAPIで扱うデータ構造が古いバージョンのデータ構造を包括する形であり、古いバージョンのAPIを想定しているプログラムを使用しているユーザーをサポートし続けられるようにすることを指します。
紹介するのは文字列型などの拡張できない型の値の配列の使用を避けてオブジェクトの配列にすると後で拡張しやすい、という方法です。例えば次のようなデータ構造があるとします。
type BreakData = {
personNameList: string[];
};
const data = {
personNameList: ['浜松太郎', '浜松次郎']
}
このデータは人名を配列として持っています。ここに「任意入力のフリガナも欲しい」という話が出てきた場合、データ構造は次のようになります。
// 後方互換性を保つ場合はいささかデータのまとまりがなくなります
type BreakData = {
personNameList: string[];
personNameKanaList: (string|null)[];
};
const data = {
personNameList: ['浜松太郎', '浜松次郎', '浜松三郎'],
personNameKanaList: ['ハママツタロウ', null, 'ハママツサブロウ'],
}
// データ構造を新しく整える場合は破壊的変更が必要になります
type BreakData = {
personList: {
name: string;
kanaName: string|null;
}[];
};
// 増築で対応することも出来ますが、新たに開発する人間が混乱しがちです
type BreakData = {
personNameList: string[];
personList: {
name: string;
kanaName: string|null;
}[];
};
同じものを指すのにインデックス参照が必要になったり、破壊的変更を起こしたりします。インデックス参照についてはアダプターパターン的に型の変換処理をはさむことで緩和できますが、手間は手間です。
代わりに、最初からオブジェクトの配列を使用することで、将来的に新しいフィールドを追加しやすくなります。これは例えば次のようになります。
type NotBreakData = {
personList: {
name: string;
}[];
};
この構造では、各名前がオブジェクトとして格納されているため、新しいプロパティを追加するのが容易です。先ほどの例のように、フリガナを追加する場合は次のようになります。
type NotBreakData = {
personList: {
name: string;
kanaName: string|null;
}[];
};
const data = {
personList: [
{ name: '浜松太郎', kanaName: 'ハママツタロウ' },
{ name: '浜松次郎', kanaName: null },
{ name: '浜松三郎', kanaName: 'ハママツサブロウ' },
]
}
必要なのが人名ではなく人物の情報であったと最初から気づけていれば自然にこのようなデータ構造を作ることになるかと思います。しかしながらある程度ならともかく確実にこの様な変化する部分全てを見抜くのは困難です。そこであらかじめプリミティブな値の配列でなくオブジェクトの配列を積極的に使用することによって、どの配列の持つデータが変わろうとも、追加のプロパティを増やすだけで済むように予め構えておく手法をとります。
予めオブジェクトの配列としておく利点を述べましたが、欠点もあります。それは拡張が成されなかった場合、ただ余分な構造を持つだけのデータ構造になってしまうことです。こうなる場合、単にプリミティブな値の配列である方が読みやすいし、扱いやすいです。紹介した方法はあくまで今後を見据えておく方法です。
オブジェクトの配列を積極的に使用することで、後方互換性を保ちつつデータ構造の変更を容易にすることができます。この方法は将来的な拡張を予測しづらい場合に特に有効です。
余談ですが、後で拡張する時はプロパティを生やすだけにしたい、という方針を定めるとJSONのトップレベルはオブジェクトで始めたくなります。これは次のような事態を防ぐためです。
// このデータにバージョンのメタ情報を持たせたい
[
{ "value": 1 },
{ "value": 2 },
{ "value": 3 }
]
// 0番データにはメタデータも含まれる的な対応をしないと後方互換性を保てない
[
{ "value": 1, "version": "0.1.0" },
{ "value": 2 },
{ "value": 3 }
]
// トップレベルはオブジェクトの構造にしておく
{
"data": [
{ "value": 1 },
{ "value": 2 },
{ "value": 3 }
]
}
// これなら自然にバージョン情報を追加できる
{
"version": "0.1.0",
"data": [
{ "value": 1 },
{ "value": 2 },
{ "value": 3 }
]
}