アプリを作っていると、似たようなオブジェクトを数千、数万個も生成することがあります。地図上に数万個のマーカーを配置する機能が代表例で、このような場合はメモリグラフが上がり続けやすくなります。
そんなときに検討したいのがフライウェイトパターンです。複数のオブジェクトが共通して使うデータを1つにまとめて共有し、メモリ使用量を大幅に減らす手法です。同じものを毎回作り直さず、作成済みの1つを一緒に使います。
この記事では、Swiftにおけるフライウェイトパターンの概要、適した場面、実際のコードでの実装方法まで解説します。
フライウェイトパターンとは?
Flyweightは「軽い物体」という意味です。ボクシングで最も軽い階級の名前でもあります。
考え方は単純です。オブジェクトが持つデータを2種類に分けます。
- 内在的状態(intrinsic):複数のオブジェクトで共有できる、変化しないデータ。木の種類、テクスチャ、色などが例です。
- 外在的状態(extrinsic):オブジェクトごとに異なり、その都度変化するデータ。木の位置や大きさなどが例です。
森を描くと考えてみましょう。100万本の木を描くとしても、実際には松、オーク、カエデの3種類だけだったらどうでしょうか?
テクスチャと色の情報(内在的状態)は3つだけ作って共有し、位置の値(外在的状態)だけを100万個別々に持てばよいのです。
重い共通データは1つだけ作って共有し、軽い個別データだけを別々に管理する。これがフライウェイトのすべてです。
Swiftのコードで実装してみましょう
言葉だけではイメージしにくいので、すぐにコードを見てみましょう。先ほどの木の例をそのままコードにします。
まず、共有する内在的状態を保持するフライウェイトオブジェクトです。
// 共有される重いデータ(内在的状態)
final class TreeType {
let name: String
let color: String
let texture: Data // 容量の大きいテクスチャ
init(name: String, color: String, texture: Data) {
self.name = name
self.color = color
self.texture = texture
}
}
次に、これらのTreeTypeを管理して共有するファクトリを作ります。すでに作成済みの種類があれば、新たに作らずそのまま使います。
// 同じ種類は新しく作らず再利用
final class TreeFactory {
private var pool: [String: TreeType] = [:]
func treeType(name: String, color: String, texture: Data) -> TreeType {
let key = "\(name)-\(color)"
if let existing = pool[key] { return existing }
let type = TreeType(name: name, color: color, texture: texture)
pool[key] = type
return type
}
}
実際の木は位置などの外在的状態だけを持ち、重いデータはTreeTypeを参照します。
// 個々の木は位置だけを持ち、種類は参照で共有
struct Tree {
let x: Double
let y: Double
let type: TreeType // 共有オブジェクトへの参照
}
木を100万本植えても、TreeTypeのインスタンスは種類の数(例:3つ)だけ存在します。残りはすべて、その3つを指す参照にすぎません。
いつ使い、いつ使わないべきか?
フライウェイトが常に正解とは限りません。使わないほうがよい場合も多くあります。状況ごとに整理します。
| 状況 | フライウェイト |
|---|---|
| 同じ性質のオブジェクトを非常に大量に生成する | 適している |
| オブジェクトが共有可能な重いデータを持つ | 適している |
| オブジェクト数が少ない | 不要(むしろ複雑さが増すだけ) |
| 状態の大半がオブジェクトごとに異なる | 効果は小さい |
大量のオブジェクトと共有可能な重いデータ。この2つの条件が重なるときこそ、フライウェイトの出番です。
Swiftでは、classは参照型としてすでに参照で共有されます。そのため、値型のstructに重いデータを丸ごと入れて何度もコピーしてしまう状況で特に効果を発揮します。
よくある質問
Q. Singleton Patternとは何が違いますか?
Singletonはクラスのインスタンスを1つだけに強制するパターンです。フライウェイトは複数のインスタンスを持ちながら、共有できるものだけを選んで共有します。木の種類は3つ、木そのものは100万本、という具合です。
Q. キャッシュと混同してしまいます。
目的が異なります。キャッシュは「再計算・再取得したくない」ために結果を保存し、フライウェイトは「メモリを節約する」ために共有オブジェクトを管理します。ただし、ファクトリのpoolはキャッシュのように動作します。
Q. Swift Standard Libraryでも使われていますか?
はい。文字列インターニングや小さな整数のキャッシュなど、似た発想の最適化があります。同じ値を毎回新しく作らず、共有するのです。
フライウェイトパターンは名前こそ難しそうですが、発想はとても単純です。「重複する重いものは1つだけ作って共有しよう」ということです。
地図のマーカー、パーティクル、タイルマップ、テキストレンダリングのように、似たオブジェクトを大量に扱う必要があるなら、一度検討してみてください。メモリグラフが目に見えて落ち着くのを実感できるはずです。

