Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Flyweight Pattern! ๐ In this guide, weโll explore how to optimize memory usage in TypeScript applications by sharing common data efficiently.
Youโll discover how the Flyweight Pattern can transform your TypeScript development experience when building applications that need to handle thousands of similar objects. Whether youโre creating games ๐ฎ, rendering graphics ๐จ, or managing large datasets ๐, understanding the Flyweight Pattern is essential for writing memory-efficient code.
By the end of this tutorial, youโll feel confident using the Flyweight Pattern to optimize your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Flyweight Pattern
๐ค What is the Flyweight Pattern?
The Flyweight Pattern is like a car-sharing service ๐. Think of it as sharing common resources among multiple users instead of everyone owning their own car. Just like how car-sharing reduces the total number of cars needed in a city, the Flyweight Pattern reduces memory usage by sharing common data!
In TypeScript terms, the Flyweight Pattern separates an objectโs intrinsic state (shared data) from its extrinsic state (unique data). This means you can:
- โจ Share common data across thousands of objects
- ๐ Reduce memory footprint dramatically
- ๐ก๏ธ Maintain object-oriented design principles
๐ก Why Use the Flyweight Pattern?
Hereโs why developers love the Flyweight Pattern:
- Memory Efficiency ๐: Store shared data only once
- Performance Boost ๐ป: Less memory allocation means faster operations
- Scalability ๐: Handle millions of objects without memory issues
- Clean Architecture ๐ง: Separate concerns between shared and unique data
Real-world example: Imagine building a text editor ๐. With the Flyweight Pattern, you can share font and style information among thousands of characters instead of storing it for each one!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Flyweight Pattern!
interface TreeType {
name: string; // ๐ณ Tree species
color: string; // ๐จ Leaf color
texture: string; // ๐ผ๏ธ Bark texture
// ๐ฏ Draw method uses extrinsic state
draw(x: number, y: number): void;
}
// ๐๏ธ Concrete flyweight
class ConcreteTreeType implements TreeType {
constructor(
public name: string,
public color: string,
public texture: string
) {}
draw(x: number, y: number): void {
console.log(`๐ณ Drawing ${this.name} tree at (${x}, ${y})`);
}
}
// ๐ญ Flyweight factory
class TreeFactory {
private static treeTypes: Map<string, TreeType> = new Map();
// โจ Get or create tree type
static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}_${color}_${texture}`;
if (!this.treeTypes.has(key)) {
console.log(`๐ Creating new tree type: ${name}`);
this.treeTypes.set(key, new ConcreteTreeType(name, color, texture));
}
return this.treeTypes.get(key)!;
}
// ๐ Show memory savings
static getTreeTypeCount(): number {
return this.treeTypes.size;
}
}
๐ก Explanation: Notice how we use a factory to manage shared tree types! The factory ensures we only create one instance of each unique tree type combination.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐ฒ Individual tree with position (extrinsic state)
class Tree {
constructor(
private x: number, // ๐ X position
private y: number, // ๐ Y position
private type: TreeType // ๐ณ Shared tree type
) {}
// ๐จ Draw this tree
draw(): void {
this.type.draw(this.x, this.y);
}
}
// ๐๏ธ Forest manager
class Forest {
private trees: Tree[] = [];
// ๐ฑ Plant a tree
plantTree(x: number, y: number, name: string, color: string, texture: string): void {
const type = TreeFactory.getTreeType(name, color, texture);
const tree = new Tree(x, y, type);
this.trees.push(tree);
}
// ๐จ Draw all trees
draw(): void {
console.log(`๐๏ธ Drawing forest with ${this.trees.length} trees...`);
this.trees.forEach(tree => tree.draw());
}
// ๐ Show memory savings
showStats(): void {
console.log(`๐ Forest Stats:`);
console.log(` ๐ณ Total trees: ${this.trees.length}`);
console.log(` ๐พ Unique tree types: ${TreeFactory.getTreeTypeCount()}`);
console.log(` ๐ Memory saved: ${((1 - TreeFactory.getTreeTypeCount() / this.trees.length) * 100).toFixed(1)}%`);
}
}
๐ก Practical Examples
๐ Example 1: Text Editor Characters
Letโs build something real:
// ๐ Character formatting (intrinsic state)
interface CharacterFormat {
font: string; // ๐ค Font family
size: number; // ๐ Font size
color: string; // ๐จ Text color
bold: boolean; // ๐
ฑ๏ธ Bold style
italic: boolean; // ๐
ธ๏ธ Italic style
}
// โจ Flyweight character format
class SharedCharacterFormat implements CharacterFormat {
constructor(
public font: string,
public size: number,
public color: string,
public bold: boolean,
public italic: boolean
) {}
// ๐ Create unique key
getKey(): string {
return `${this.font}_${this.size}_${this.color}_${this.bold}_${this.italic}`;
}
}
// ๐ญ Format factory
class FormatFactory {
private static formats: Map<string, SharedCharacterFormat> = new Map();
// ๐จ Get or create format
static getFormat(
font: string,
size: number,
color: string,
bold: boolean,
italic: boolean
): SharedCharacterFormat {
const format = new SharedCharacterFormat(font, size, color, bold, italic);
const key = format.getKey();
if (!this.formats.has(key)) {
console.log(`๐ Creating new format: ${key}`);
this.formats.set(key, format);
}
return this.formats.get(key)!;
}
// ๐ Get statistics
static getFormatCount(): number {
return this.formats.size;
}
}
// ๐ Individual character
class Character {
constructor(
private char: string, // ๐ค The actual character
private position: number, // ๐ Position in document
private format: SharedCharacterFormat // ๐จ Shared formatting
) {}
// ๐จ๏ธ Display character
display(): string {
const style = this.format.bold ? '**' : '';
const italic = this.format.italic ? '_' : '';
return `${style}${italic}${this.char}${italic}${style}`;
}
}
// ๐ Text editor
class TextEditor {
private characters: Character[] = [];
// โ๏ธ Add text with formatting
addText(text: string, font: string, size: number, color: string, bold: boolean, italic: boolean): void {
const format = FormatFactory.getFormat(font, size, color, bold, italic);
for (let i = 0; i < text.length; i++) {
const char = new Character(text[i], this.characters.length, format);
this.characters.push(char);
}
console.log(`โ๏ธ Added "${text}" with format`);
}
// ๐ Display document
display(): void {
console.log("๐ Document content:");
const content = this.characters.map(char => char.display()).join('');
console.log(content);
}
// ๐ Show memory efficiency
showStats(): void {
console.log(`๐ Editor Stats:`);
console.log(` ๐ค Total characters: ${this.characters.length}`);
console.log(` ๐จ Unique formats: ${FormatFactory.getFormatCount()}`);
console.log(` ๐พ Memory efficiency: ${((1 - FormatFactory.getFormatCount() / this.characters.length) * 100).toFixed(1)}%`);
}
}
// ๐ฎ Let's use it!
const editor = new TextEditor();
editor.addText("Hello ", "Arial", 12, "black", false, false);
editor.addText("TypeScript", "Arial", 12, "blue", true, false);
editor.addText("! ๐", "Arial", 12, "black", false, false);
editor.display();
editor.showStats();
๐ฏ Try it yourself: Add a method to change formatting for a range of characters!
๐ฎ Example 2: Game Particle System
Letโs make it fun:
// ๐ Particle type (intrinsic state)
interface ParticleType {
texture: string; // ๐ผ๏ธ Particle texture
color: string; // ๐จ Base color
size: number; // ๐ Base size
behavior: string; // ๐ญ Movement pattern
}
// โจ Concrete particle type
class ConcreteParticleType implements ParticleType {
constructor(
public texture: string,
public color: string,
public size: number,
public behavior: string
) {}
// ๐ฏ Create unique identifier
getId(): string {
return `${this.texture}_${this.color}_${this.size}_${this.behavior}`;
}
}
// ๐ญ Particle factory
class ParticleFactory {
private static types: Map<string, ParticleType> = new Map();
// ๐ Get or create particle type
static getParticleType(
texture: string,
color: string,
size: number,
behavior: string
): ParticleType {
const type = new ConcreteParticleType(texture, color, size, behavior);
const id = type.getId();
if (!this.types.has(id)) {
console.log(`๐ Creating particle type: ${texture}`);
this.types.set(id, type);
}
return this.types.get(id)!;
}
// ๐ Get type count
static getTypeCount(): number {
return this.types.size;
}
}
// ๐ Individual particle (extrinsic state)
class Particle {
constructor(
public x: number, // ๐ X position
public y: number, // ๐ Y position
public velocity: number, // ๐ Speed
public direction: number, // ๐งญ Direction in degrees
public lifespan: number, // โฑ๏ธ Remaining life
private type: ParticleType // ๐ Shared type
) {}
// ๐ Update particle
update(deltaTime: number): void {
const radians = this.direction * Math.PI / 180;
this.x += Math.cos(radians) * this.velocity * deltaTime;
this.y += Math.sin(radians) * this.velocity * deltaTime;
this.lifespan -= deltaTime;
}
// ๐จ Render particle
render(): void {
if (this.lifespan > 0) {
console.log(`โจ Particle at (${this.x.toFixed(1)}, ${this.y.toFixed(1)})`);
}
}
// ๐ Check if dead
isDead(): boolean {
return this.lifespan <= 0;
}
}
// ๐ฎ Particle system manager
class ParticleSystem {
private particles: Particle[] = [];
private maxParticles: number = 10000;
// ๐ Emit particles
emit(
count: number,
x: number,
y: number,
texture: string,
color: string,
size: number,
behavior: string
): void {
const type = ParticleFactory.getParticleType(texture, color, size, behavior);
for (let i = 0; i < count; i++) {
if (this.particles.length >= this.maxParticles) break;
const particle = new Particle(
x,
y,
Math.random() * 100 + 50, // ๐ฒ Random velocity
Math.random() * 360, // ๐ฒ Random direction
Math.random() * 2 + 1, // ๐ฒ Random lifespan
type
);
this.particles.push(particle);
}
console.log(`๐ Emitted ${count} particles!`);
}
// ๐ Update all particles
update(deltaTime: number): void {
this.particles = this.particles.filter(particle => {
particle.update(deltaTime);
return !particle.isDead();
});
}
// ๐ Show system stats
showStats(): void {
console.log(`๐ฎ Particle System Stats:`);
console.log(` โจ Active particles: ${this.particles.length}`);
console.log(` ๐จ Unique particle types: ${ParticleFactory.getTypeCount()}`);
console.log(` ๐พ Memory optimization: ${((1 - ParticleFactory.getTypeCount() / this.particles.length) * 100).toFixed(1)}%`);
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Composite Flyweights
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Complex flyweight with nested shared data
interface ComplexFlyweight {
primaryData: SharedData; // ๐จ Primary shared data
secondaryData: SharedData; // ๐ญ Secondary shared data
render(context: RenderContext): void;
}
// ๐๏ธ Shared data structure
class SharedData {
constructor(
public id: string,
public properties: Map<string, any>
) {}
}
// ๐ช Render context (extrinsic)
interface RenderContext {
x: number;
y: number;
scale: number;
rotation: number;
opacity: number;
}
// ๐ญ Advanced factory with caching
class AdvancedFlyweightFactory {
private static cache: Map<string, ComplexFlyweight> = new Map();
private static sharedDataCache: Map<string, SharedData> = new Map();
// ๐ช Create complex flyweight
static createComplexFlyweight(
primaryId: string,
secondaryId: string,
primaryProps: Map<string, any>,
secondaryProps: Map<string, any>
): ComplexFlyweight {
const key = `${primaryId}_${secondaryId}`;
if (!this.cache.has(key)) {
const primary = this.getOrCreateSharedData(primaryId, primaryProps);
const secondary = this.getOrCreateSharedData(secondaryId, secondaryProps);
const flyweight: ComplexFlyweight = {
primaryData: primary,
secondaryData: secondary,
render(context: RenderContext): void {
console.log(`โจ Rendering complex object at (${context.x}, ${context.y})`);
}
};
this.cache.set(key, flyweight);
}
return this.cache.get(key)!;
}
// ๐พ Get or create shared data
private static getOrCreateSharedData(id: string, properties: Map<string, any>): SharedData {
if (!this.sharedDataCache.has(id)) {
this.sharedDataCache.set(id, new SharedData(id, properties));
}
return this.sharedDataCache.get(id)!;
}
}
๐๏ธ Advanced Topic 2: Thread-Safe Flyweight
For the brave developers:
// ๐ Thread-safe flyweight pool
class ThreadSafeFlyweightPool<T> {
private pool: T[] = [];
private inUse: Set<T> = new Set();
private factory: () => T;
private maxSize: number;
constructor(factory: () => T, maxSize: number = 100) {
this.factory = factory;
this.maxSize = maxSize;
}
// ๐ฏ Acquire flyweight from pool
acquire(): T | null {
let flyweight: T | undefined;
// ๐ Try to find available flyweight
for (const item of this.pool) {
if (!this.inUse.has(item)) {
flyweight = item;
break;
}
}
// ๐ Create new if needed and pool not full
if (!flyweight && this.pool.length < this.maxSize) {
flyweight = this.factory();
this.pool.push(flyweight);
}
// ๐ Mark as in use
if (flyweight) {
this.inUse.add(flyweight);
console.log(`โ
Acquired flyweight (${this.inUse.size}/${this.pool.length} in use)`);
}
return flyweight || null;
}
// ๐ Release flyweight back to pool
release(flyweight: T): void {
if (this.inUse.has(flyweight)) {
this.inUse.delete(flyweight);
console.log(`โป๏ธ Released flyweight (${this.inUse.size}/${this.pool.length} in use)`);
}
}
// ๐ Get pool statistics
getStats(): { total: number; inUse: number; available: number } {
return {
total: this.pool.length,
inUse: this.inUse.size,
available: this.pool.length - this.inUse.size
};
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Modifying Shared State
// โ Wrong way - modifying shared state!
class BadFlyweight {
public sharedData: string[] = [];
addData(item: string): void {
this.sharedData.push(item); // ๐ฅ This affects all instances!
}
}
// โ
Correct way - keep shared state immutable!
class GoodFlyweight {
constructor(
private readonly sharedData: ReadonlyArray<string>
) {}
// ๐ก๏ธ Return new array instead of modifying
addData(item: string): ReadonlyArray<string> {
return [...this.sharedData, item];
}
}
๐คฏ Pitfall 2: Not Identifying Intrinsic vs Extrinsic
// โ Dangerous - everything is intrinsic!
class BadCharacter {
constructor(
public font: string,
public size: number,
public color: string,
public x: number, // ๐ฅ Position should be extrinsic!
public y: number // ๐ฅ Position should be extrinsic!
) {}
}
// โ
Safe - proper separation!
class CharacterType {
constructor(
public readonly font: string,
public readonly size: number,
public readonly color: string
) {}
}
class GoodCharacter {
constructor(
private x: number, // โ
Extrinsic
private y: number, // โ
Extrinsic
private type: CharacterType // โ
Intrinsic (shared)
) {}
}
๐ ๏ธ Best Practices
- ๐ฏ Identify Shared State: Carefully analyze what data can be shared
- ๐ Keep Flyweights Immutable: Never modify shared state
- ๐ก๏ธ Use Factory Pattern: Centralize flyweight creation and management
- ๐จ Separate Concerns: Clearly distinguish intrinsic from extrinsic state
- โจ Monitor Memory Usage: Track your memory savings with statistics
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Chess Game Renderer
Create a memory-efficient chess game renderer:
๐ Requirements:
- โ Chess pieces with shared textures and models
- ๐ท๏ธ Different piece types (pawn, rook, knight, etc.)
- ๐ค Two colors (black and white)
- ๐ Board positions as extrinsic state
- ๐จ Each piece type needs a unique emoji!
๐ Bonus Points:
- Add move validation
- Implement piece animation states
- Create a move history tracker
๐ก Solution
๐ Click to see solution
// ๐ฏ Our memory-efficient chess system!
interface ChessPieceType {
name: string;
emoji: string;
color: "white" | "black";
movePattern: string;
}
// โ๏ธ Concrete piece type (flyweight)
class ConcreteChessPieceType implements ChessPieceType {
constructor(
public name: string,
public emoji: string,
public color: "white" | "black",
public movePattern: string
) {}
getKey(): string {
return `${this.name}_${this.color}`;
}
}
// ๐ญ Chess piece factory
class ChessPieceFactory {
private static pieces: Map<string, ChessPieceType> = new Map();
static getPiece(name: string, color: "white" | "black"): ChessPieceType {
const emoji = this.getEmoji(name, color);
const piece = new ConcreteChessPieceType(name, emoji, color, this.getMovePattern(name));
const key = piece.getKey();
if (!this.pieces.has(key)) {
console.log(`๐ Creating ${color} ${name}`);
this.pieces.set(key, piece);
}
return this.pieces.get(key)!;
}
private static getEmoji(name: string, color: "white" | "black"): string {
const emojis: Record<string, Record<"white" | "black", string>> = {
king: { white: "โ", black: "โ" },
queen: { white: "โ", black: "โ" },
rook: { white: "โ", black: "โ" },
bishop: { white: "โ", black: "โ" },
knight: { white: "โ", black: "โ" },
pawn: { white: "โ", black: "โ" }
};
return emojis[name][color];
}
private static getMovePattern(name: string): string {
const patterns: Record<string, string> = {
king: "one square any direction",
queen: "any direction any distance",
rook: "horizontal/vertical any distance",
bishop: "diagonal any distance",
knight: "L-shape",
pawn: "forward one, capture diagonal"
};
return patterns[name];
}
static getPieceCount(): number {
return this.pieces.size;
}
}
// ๐ Chess piece instance
class ChessPiece {
constructor(
private row: number,
private col: number,
private type: ChessPieceType
) {}
move(newRow: number, newCol: number): void {
console.log(`${this.type.emoji} Moving ${this.type.name} from (${this.row},${this.col}) to (${newRow},${newCol})`);
this.row = newRow;
this.col = newCol;
}
getPosition(): [number, number] {
return [this.row, this.col];
}
getType(): ChessPieceType {
return this.type;
}
}
// โ๏ธ Chess board
class ChessBoard {
private board: (ChessPiece | null)[][] = [];
private pieces: ChessPiece[] = [];
constructor() {
// ๐ Initialize empty board
for (let i = 0; i < 8; i++) {
this.board[i] = new Array(8).fill(null);
}
this.setupBoard();
}
private setupBoard(): void {
// ๐ Setup back row pieces
const backRowPieces = ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"];
// โช White pieces
backRowPieces.forEach((pieceName, col) => {
this.addPiece(0, col, pieceName, "white");
});
// โ๏ธ White pawns
for (let col = 0; col < 8; col++) {
this.addPiece(1, col, "pawn", "white");
}
// โซ Black pieces
backRowPieces.forEach((pieceName, col) => {
this.addPiece(7, col, pieceName, "black");
});
// โ๏ธ Black pawns
for (let col = 0; col < 8; col++) {
this.addPiece(6, col, "pawn", "black");
}
}
private addPiece(row: number, col: number, name: string, color: "white" | "black"): void {
const pieceType = ChessPieceFactory.getPiece(name, color);
const piece = new ChessPiece(row, col, pieceType);
this.board[row][col] = piece;
this.pieces.push(piece);
}
display(): void {
console.log("\nโ๏ธ Chess Board:");
console.log(" a b c d e f g h");
for (let row = 7; row >= 0; row--) {
let rowStr = `${row + 1} `;
for (let col = 0; col < 8; col++) {
const piece = this.board[row][col];
rowStr += piece ? piece.getType().emoji + " " : "ยท ";
}
console.log(rowStr);
}
}
showStats(): void {
console.log("\n๐ Chess Board Stats:");
console.log(` โ๏ธ Total pieces: ${this.pieces.length}`);
console.log(` ๐พ Unique piece types: ${ChessPieceFactory.getPieceCount()}`);
console.log(` ๐ Memory efficiency: ${((1 - ChessPieceFactory.getPieceCount() / this.pieces.length) * 100).toFixed(1)}%`);
}
}
// ๐ฎ Test it out!
const chessGame = new ChessBoard();
chessGame.display();
chessGame.showStats();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create memory-efficient applications with confidence ๐ช
- โ Avoid common flyweight mistakes that trip up beginners ๐ก๏ธ
- โ Apply the pattern in real projects ๐ฏ
- โ Debug memory issues like a pro ๐
- โ Build scalable systems with TypeScript! ๐
Remember: The Flyweight Pattern is your friend when dealing with thousands of similar objects! Itโs here to help you write memory-efficient code. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Flyweight Pattern!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a particle system or text editor using the Flyweight Pattern
- ๐ Move on to our next tutorial: Proxy Pattern: Controlled Access
- ๐ Share your memory optimization success stories!
Remember: Every TypeScript expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ