
Lazy Initialization は「遅延初期化」、
Proxy Cache は「キャッシュ付きプロキシ」、
これらのデザインパターンを解説したいと思います。
Web系では、画像の遅延読み込みや、読み込み完了後イベント発火などの処理を行うことが多いので、
Lazy Initializationを使って、効率的にしたいのと同時に、
Proxy Cacheは、実行結果をキャッシュ保持して、同じ処理が再発する時に高速に処理できるパターンです。
この2つの定義パターンを利用して、効率的な設計にするというのが目的になります。
Lazy Initialization(遅延初期化)
・必要になるまで
オブジェクトの生成や重い処理を行わない手法。
・
メモリ消費・初期化コストを最小化できる。
・「使う直前まで準備しない」ことで
パフォーマンスを最適化する。
・ファイル読み込み・DB接続・APIクライアントなど“重いインスタンス”でよく使う。
・ただし初回アクセス時に
遅延が発生する点がデメリット。
Proxy Cache(キャッシュ付きプロキシ)
・本体オブジェクトの“代理”として動作するProxyを置き、結果をキャッシュして返す仕組み。
・初回は本体へアクセスし、結果を保存、以降はProxyがキャッシュから高速返却する。
・高負荷API、
計算コストが高い処理で特に効果的。
・本体(Real Object)へのアクセス頻度を下げ、
負荷やコストを削減する。
・キャッシュ失効・容量管理などのポリシーを設計する必要あり。
両者比較
| 観点<>/th>
| Lazy Initialization |
Proxy Cache |
| 主目的 |
初期化コスト削減 |
アクセス回数削減 |
| 初回アクセス |
遅い(遅延初期化が起きる) |
通常通り |
| 2回目以降 |
毎回処理 |
速い(キャッシュ返却) |
| 遅延要素 |
オブジェクト生成 |
計算・取得結果の再利用 |
| 主な用途 |
“重い初期化”の回避 |
“重い計算やI/O”の高速化 |
Lazy Initialization × Proxy のよくある併用例
・初回アクセス時にオブジェクトを生成(Lazy)。
・生成済みならキャッシュされたオブジェクトを返す(Proxy)。
・「初回だけ重く、その後は高速」という超定番パターン。
サンプルコード1
Lazy Initialization
class HeavyService {
constructor() {
console.log("Heavy object created");
}
}
let service = null;
function getService() {
if (!service) {
service = new HeavyService(); // 初回だけ生成
}
return service;
}
getService(); // 生成
getService(); // キャッシュされた同じオブジェクト
Proxy Cache
function createCached(fn) {
let cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
let key = JSON.stringify(args);
if (cache.has(key)) {
console.log("from cache");
return cache.get(key);
}
let result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
function heavyCalc(n) {
console.log("heavy calc");
return n * n;
}
const cachedCalc = createCached(heavyCalc);
cachedCalc(5); // heavy
cachedCalc(5); // cache
サンプルコード2
Lazy Initialization × Proxy Cache の統合パターンサンプルです。
1. APIクライアント
初回だけ重い初期化を行い、以降はキャッシュ結果を返すのがポイントです。
想定シナリオ
1. APIクライアント生成は重い(トークン取得・設定ロードなど)
2. 同じリクエストが何度も来る
3. 初回だけ生成、以降はキャッシュして高速化
// === Lazy + Proxy Cache 合体パターン ===
class RealApiClient {
constructor() {
this.token = this.fetchToken(); // 重い初期化
}
fetchToken() {
console.log("APIトークン取得…(重い)");
return "TOKEN-XYZ";
}
async fetchUser(id) {
console.log("APIアクセス /users/" + id);
return { id, name: "User " + id };
}
}
class ApiClientProxy {
constructor() {
this.real = null; // Lazy初期化ポイント
this.cache = new Map(); // Proxy Cache
}
async fetchUser(id) {
if (!this.real) this.real = new RealApiClient(); // Lazy
if (this.cache.has(id)) return this.cache.get(id); // Cache
const result = await this.real.fetchUser(id);
this.cache.set(id, result);
return result;
}
}
// ==== 使用例 ====
(async () => {
const api = new ApiClientProxy();
console.log(await api.fetchUser(1)); // 初回 → トークン取得 → APIアクセス
console.log(await api.fetchUser(1)); // 2回目 → キャッシュだけ
})();
2. DBコネクション(接続は Lazy、クエリ結果を Proxy Cache)
想定シナリオ
1. DB接続は重いから「必要になるまで行わない」
2. 同じ SELECT を何度も実行する
3. キャッシュを Proxy が巻き取る
class RealDatabase {
constructor() {
this.connect(); // 重い処理
}
connect() {
console.log("DB接続(重い)...");
}
query(sql) {
console.log("SQL実行: " + sql);
return { rows: [ { id: 1, name: "Alice" } ] };
}
}
class DatabaseProxy {
constructor() {
this.real = null; // Lazy DB
this.cache = new Map(); // Query Cache
}
query(sql) {
if (!this.real) this.real = new RealDatabase(); // Lazy
if (this.cache.has(sql)) return this.cache.get(sql); // Cache
const result = this.real.query(sql);
this.cache.set(sql, result);
return result;
}
}
// 使う
const db = new DatabaseProxy();
db.query("SELECT * FROM users"); // 接続 + 実行
db.query("SELECT * FROM users"); // キャッシュのみ
3. 設定ロード(Lazy読み込み → 設定アクセスを Proxy が高速化)
想定シナリオ
1. 設定ファイル読み込みは重い(大きなJSON)
2. アプリの複数箇所から参照される
3. 1回だけロードして、その後はキャッシュから返す
class RealConfig {
constructor() {
this.config = this.load(); // 重い
}
load() {
console.log("設定ファイルを読み込み中…(重い)");
return {
apiUrl: "https://example.com",
retry: 3
};
}
get(key) {
return this.config[key];
}
}
class ConfigProxy {
constructor() {
this.real = null;
}
get(key) {
if (!this.real) this.real = new RealConfig(); // Lazy
return this.real.get(key); // Cacheは内部で保持
}
}
// 使用例
const config = new ConfigProxy();
console.log(config.get("apiUrl")); // 初回だけ重い
console.log(config.get("retry")); // 2回目以降は軽い
メリット
Lazy Initialization(遅延初期化)
・初期化コストを最小化でき、アプリ起動が速くなる
・必要になるまでオブジェクトが生成されず、メモリ消費を抑えられる
・重いリソース(DB接続・ファイルロードなど)を後回しにできる
・実際に使われない可能性のある機能の“無駄な初期化”を避けられる
・問題発生時に「初期化ポイントが明確」で原因調査しやすい
Proxy Cache(キャッシュ付きプロキシ)
・高コスト処理(計算・APIアクセス)を劇的に高速化
・本体オブジェクトへのアクセス頻度を減らし、負荷を下げる
・同じ入力に対して一貫した結果を返す(冪等性が高まる)
・実装次第で「キャッシュ戦略(TTL・LRUなど)」を差し替えやすい
・本体側を変更せず「横断的な高速化」を注入できる(Open/Closed原則に適合)
デメリット
Lazy Initialization(遅延初期化)
・初回アクセス時に「一度だけ重い処理」が走る
・遅延することで「ユーザーの操作タイミングと遅延が重なる」可能性
・マルチスレッド環境では“遅延初期化の排他制御”が必要
・初期化タイミングがコード上で見えにくく、設計が複雑化しやすい
・依存が埋もれて、テストがしにくくなることがある
Proxy Cache(キャッシュ付きプロキシ)
・キャッシュ管理(容量・期限)の設計が必要
・キャッシュが古くなる(Stale Data)のリスク
・メモリ使用量が増加する
・キャッシュのヒット率が低いと、逆にコスト増
・キャッシュ無効化(purge)の仕組みが必要になる場合が多い
あとがき
メモリ管理をしないといけないのは、C言語だけなどと考えている場合じゃ無いです。
メモリ効率を考えるのも、プログラミングの醍醐味。
今回のパターンは、処理を遅くしないための効率的な構造を作るための設計しそうです。
色々な重い処理を繰り返すようなシステムの場合に力を発揮します。
なかなか大掛かりな設計に見えてしまいますが、こなれてくると、コツのようなものが見えてくると思います。
メモリボリュームを頭にイメージして設計できるようになると、使いこなせるようになるでしょうね。
0 件のコメント:
コメントを投稿