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
Hey there, TypeScript explorer! ๐ Have you ever been to a party where thereโs only ONE DJ booth? ๐ง No matter how many people want to play music, they all have to use the same equipment. Thatโs exactly what the Singleton pattern is all about โ ensuring thereโs only ONE instance of something in your entire application!
Today, weโre diving into one of the most famous (and sometimes controversial) design patterns. Whether youโre building a game engine ๐ฎ, managing app settings โ๏ธ, or handling database connections ๐๏ธ, the Singleton pattern is your ticket to maintaining control and consistency. Letโs master this powerful pattern together! ๐ช
๐ Understanding Singleton Pattern
Think of the Singleton pattern like the president of a country ๐๏ธ โ there can only be ONE at any given time! No matter how many times people refer to โthe president,โ theyโre all talking about the same person. Thatโs the essence of Singleton!
What Makes a Singleton Special? ๐
The Singleton pattern ensures:
- Single Instance: Only ONE instance exists throughout your appโs lifetime
- Global Access: Available from anywhere in your application
- Lazy Initialization: Created only when first needed
- Thread Safety: Handles concurrent access properly (weโll cover this!)
Itโs like having a single remote control ๐บ for your TV โ everyone in the house uses the same one!
๐ง Basic Syntax and Usage
Letโs start with a simple Singleton implementation in TypeScript! ๐
// ๐ Basic Singleton class
class ConfigManager {
private static instance: ConfigManager;
private config: Record<string, any> = {};
// ๐ซ Private constructor prevents direct instantiation
private constructor() {
console.log("๐ ConfigManager initialized!");
}
// ๐ฏ Static method to get the instance
public static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
// ๐ Add configuration
public set(key: string, value: any): void {
this.config[key] = value;
}
// ๐ Get configuration
public get(key: string): any {
return this.config[key];
}
}
// ๐ฎ Let's use it!
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true โ
Same instance!
config1.set("theme", "๐ dark");
console.log(config2.get("theme")); // "๐ dark" - Same data!
See how both config1
and config2
point to the exact same instance? Thatโs the magic! โจ
๐ก Practical Examples
Letโs explore some real-world scenarios where Singleton shines! ๐
Example 1: Game Score Manager ๐ฎ
// ๐ Managing game scores across your app
class ScoreManager {
private static instance: ScoreManager;
private scores: Map<string, number> = new Map();
private highScore: number = 0;
private constructor() {
console.log("๐ฏ ScoreManager ready!");
}
public static getInstance(): ScoreManager {
if (!ScoreManager.instance) {
ScoreManager.instance = new ScoreManager();
}
return ScoreManager.instance;
}
// ๐ Add player score
public addScore(player: string, points: number): void {
const currentScore = this.scores.get(player) || 0;
const newScore = currentScore + points;
this.scores.set(player, newScore);
// ๐
Update high score if needed
if (newScore > this.highScore) {
this.highScore = newScore;
console.log(`๐ New high score: ${newScore} by ${player}!`);
}
}
// ๐ Get leaderboard
public getLeaderboard(): Array<{player: string, score: number}> {
return Array.from(this.scores.entries())
.map(([player, score]) => ({ player, score }))
.sort((a, b) => b.score - a.score);
}
// ๐ Get high score
public getHighScore(): number {
return this.highScore;
}
}
// ๐ฎ Game components using the singleton
class GameLevel {
private scoreManager = ScoreManager.getInstance();
completeLevel(player: string, levelScore: number): void {
console.log(`โ
Level complete! Adding ${levelScore} points`);
this.scoreManager.addScore(player, levelScore);
}
}
class BonusRound {
private scoreManager = ScoreManager.getInstance();
collectBonus(player: string, bonus: number): void {
console.log(`๐ Bonus collected! +${bonus} points`);
this.scoreManager.addScore(player, bonus);
}
}
// ๐ฏ Playing the game
const level = new GameLevel();
const bonus = new BonusRound();
level.completeLevel("Alice ๐ฆ", 100);
bonus.collectBonus("Alice ๐ฆ", 50);
level.completeLevel("Bob ๐ป", 120);
const scores = ScoreManager.getInstance();
console.log("๐ Leaderboard:", scores.getLeaderboard());
Example 2: Theme Manager for Your App ๐จ
// ๐จ Managing app theme consistently
interface Theme {
name: string;
primary: string;
secondary: string;
background: string;
text: string;
}
class ThemeManager {
private static instance: ThemeManager;
private currentTheme: Theme;
private themes: Map<string, Theme> = new Map();
private listeners: Array<(theme: Theme) => void> = [];
private constructor() {
// ๐ Initialize with default themes
this.themes.set("light", {
name: "โ๏ธ Light",
primary: "#3498db",
secondary: "#2ecc71",
background: "#ffffff",
text: "#2c3e50"
});
this.themes.set("dark", {
name: "๐ Dark",
primary: "#9b59b6",
secondary: "#e74c3c",
background: "#2c3e50",
text: "#ecf0f1"
});
this.themes.set("ocean", {
name: "๐ Ocean",
primary: "#006994",
secondary: "#00a8cc",
background: "#e8f5f5",
text: "#004e71"
});
this.currentTheme = this.themes.get("light")!;
}
public static getInstance(): ThemeManager {
if (!ThemeManager.instance) {
ThemeManager.instance = new ThemeManager();
}
return ThemeManager.instance;
}
// ๐จ Get current theme
public getCurrentTheme(): Theme {
return this.currentTheme;
}
// ๐ Switch theme
public switchTheme(themeName: string): void {
const theme = this.themes.get(themeName);
if (theme) {
this.currentTheme = theme;
console.log(`โจ Switched to ${theme.name} theme!`);
this.notifyListeners();
}
}
// ๐ Subscribe to theme changes
public subscribe(listener: (theme: Theme) => void): void {
this.listeners.push(listener);
}
// ๐ข Notify all listeners
private notifyListeners(): void {
this.listeners.forEach(listener => listener(this.currentTheme));
}
// ๐ Get available themes
public getAvailableThemes(): string[] {
return Array.from(this.themes.keys());
}
}
// ๐จ UI Components using the theme
class Header {
private themeManager = ThemeManager.getInstance();
constructor() {
this.themeManager.subscribe((theme) => {
console.log(`๐ฑ Header updated with ${theme.name} colors!`);
});
}
render(): void {
const theme = this.themeManager.getCurrentTheme();
console.log(`๐จ Header: Background ${theme.primary}, Text ${theme.text}`);
}
}
class Sidebar {
private themeManager = ThemeManager.getInstance();
constructor() {
this.themeManager.subscribe((theme) => {
console.log(`๐ Sidebar updated with ${theme.name} colors!`);
});
}
}
// ๐ฎ Using the theme system
const header = new Header();
const sidebar = new Sidebar();
const theme = ThemeManager.getInstance();
header.render();
theme.switchTheme("dark");
theme.switchTheme("ocean");
Example 3: Database Connection Pool ๐๏ธ
// ๐๏ธ Managing database connections efficiently
interface DBConnection {
id: string;
isActive: boolean;
query: (sql: string) => Promise<any>;
}
class DatabasePool {
private static instance: DatabasePool;
private connections: DBConnection[] = [];
private maxConnections: number = 5;
private activeQueries: number = 0;
private constructor() {
console.log("๐ Database pool initialized!");
this.initializeConnections();
}
public static getInstance(): DatabasePool {
if (!DatabasePool.instance) {
DatabasePool.instance = new DatabasePool();
}
return DatabasePool.instance;
}
// ๐ง Initialize connection pool
private initializeConnections(): void {
for (let i = 0; i < this.maxConnections; i++) {
this.connections.push({
id: `conn-${i + 1}`,
isActive: false,
query: async (sql: string) => {
// ๐ญ Simulate database query
console.log(`๐ Executing: ${sql}`);
await new Promise(resolve => setTimeout(resolve, 100));
return { success: true, data: "Query result ๐ฆ" };
}
});
}
}
// ๐ฏ Get available connection
private async getConnection(): Promise<DBConnection> {
// ๐ Find available connection
let connection = this.connections.find(conn => !conn.isActive);
if (!connection) {
console.log("โณ All connections busy, waiting...");
await new Promise(resolve => setTimeout(resolve, 200));
return this.getConnection();
}
connection.isActive = true;
this.activeQueries++;
console.log(`๐ Using connection: ${connection.id}`);
return connection;
}
// ๐ Execute query
public async executeQuery(sql: string): Promise<any> {
const connection = await this.getConnection();
try {
const result = await connection.query(sql);
console.log(`โ
Query completed on ${connection.id}`);
return result;
} finally {
// ๐ Release connection
connection.isActive = false;
this.activeQueries--;
console.log(`๐ Released connection: ${connection.id}`);
}
}
// ๐ Get pool statistics
public getStats(): { total: number; active: number; available: number } {
const active = this.connections.filter(conn => conn.isActive).length;
return {
total: this.maxConnections,
active,
available: this.maxConnections - active
};
}
}
// ๐ฎ Using the database pool
async function runDatabaseOperations() {
const db = DatabasePool.getInstance();
// ๐ Multiple queries from different parts of the app
const queries = [
db.executeQuery("SELECT * FROM users ๐ฅ"),
db.executeQuery("SELECT * FROM products ๐ฆ"),
db.executeQuery("SELECT * FROM orders ๐"),
db.executeQuery("UPDATE inventory SET stock = stock - 1 ๐"),
db.executeQuery("INSERT INTO logs VALUES ('Action logged') ๐"),
db.executeQuery("SELECT * FROM analytics ๐")
];
console.log("๐ Starting queries...");
console.log("๐ Pool stats:", db.getStats());
await Promise.all(queries);
console.log("๐ All queries completed!");
console.log("๐ Final pool stats:", db.getStats());
}
// ๐โโ๏ธ Run the example
runDatabaseOperations();
๐ Advanced Concepts
Ready to level up your Singleton game? Letโs explore some advanced techniques! ๐ฏ
Thread-Safe Singleton (for async operations) ๐
// ๐ Thread-safe singleton with async initialization
class AsyncSingleton {
private static instance: AsyncSingleton;
private static initPromise: Promise<AsyncSingleton>;
private data: any;
private constructor() {}
// ๐ Async initialization
private async initialize(): Promise<void> {
console.log("โณ Starting async initialization...");
// ๐ญ Simulate async operation (like loading config from API)
await new Promise(resolve => setTimeout(resolve, 1000));
this.data = {
apiKey: "๐ secret-key-123",
serverUrl: "๐ https://api.example.com"
};
console.log("โ
Initialization complete!");
}
// ๐ฏ Get instance with async initialization
public static async getInstance(): Promise<AsyncSingleton> {
if (!AsyncSingleton.instance) {
if (!AsyncSingleton.initPromise) {
// ๐ Ensure only one initialization happens
AsyncSingleton.initPromise = (async () => {
AsyncSingleton.instance = new AsyncSingleton();
await AsyncSingleton.instance.initialize();
return AsyncSingleton.instance;
})();
}
return AsyncSingleton.initPromise;
}
return AsyncSingleton.instance;
}
public getData(): any {
return this.data;
}
}
// ๐ฎ Using async singleton
async function useAsyncSingleton() {
console.log("๐ Getting instance 1...");
const instance1 = await AsyncSingleton.getInstance();
console.log("๐ Getting instance 2...");
const instance2 = await AsyncSingleton.getInstance();
console.log("Same instance?", instance1 === instance2); // true โ
console.log("Data:", instance1.getData());
}
useAsyncSingleton();
Singleton with Dependency Injection ๐
// ๐ Singleton that can be configured
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`๐ Console: ${message}`);
}
}
class FileLogger implements Logger {
log(message: string): void {
console.log(`๐ File: ${message}`);
}
}
class LogManager {
private static instance: LogManager;
private logger: Logger;
private logHistory: string[] = [];
private constructor(logger: Logger) {
this.logger = logger;
}
// ๐ฏ Configure and get instance
public static configure(logger: Logger): void {
if (!LogManager.instance) {
LogManager.instance = new LogManager(logger);
} else {
console.warn("โ ๏ธ LogManager already configured!");
}
}
public static getInstance(): LogManager {
if (!LogManager.instance) {
// ๐ฏ Default to console logger
LogManager.instance = new LogManager(new ConsoleLogger());
}
return LogManager.instance;
}
// ๐ Log with history
public log(level: string, message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${level}: ${message}`;
this.logger.log(logEntry);
this.logHistory.push(logEntry);
// ๐งน Keep only last 100 entries
if (this.logHistory.length > 100) {
this.logHistory.shift();
}
}
// ๐ Get log history
public getHistory(): string[] {
return [...this.logHistory];
}
// ๐จ Log with style
public info(message: string): void {
this.log("โน๏ธ INFO", message);
}
public warn(message: string): void {
this.log("โ ๏ธ WARN", message);
}
public error(message: string): void {
this.log("โ ERROR", message);
}
public success(message: string): void {
this.log("โ
SUCCESS", message);
}
}
// ๐ฎ Configure and use
LogManager.configure(new FileLogger());
const log = LogManager.getInstance();
log.info("Application started");
log.success("User logged in");
log.warn("Low memory");
log.error("Connection failed");
console.log("๐ Log history:", log.getHistory());
Registry Pattern (Multiple Named Singletons) ๐
// ๐ Managing multiple singleton instances
class ServiceRegistry {
private static instance: ServiceRegistry;
private services: Map<string, any> = new Map();
private constructor() {}
public static getInstance(): ServiceRegistry {
if (!ServiceRegistry.instance) {
ServiceRegistry.instance = new ServiceRegistry();
}
return ServiceRegistry.instance;
}
// ๐ Register a service
public register<T>(name: string, factory: () => T): void {
if (this.services.has(name)) {
console.warn(`โ ๏ธ Service '${name}' already registered!`);
return;
}
// ๐ Lazy singleton creation
let instance: T | null = null;
const serviceGetter = () => {
if (!instance) {
instance = factory();
console.log(`โจ Created singleton service: ${name}`);
}
return instance;
};
this.services.set(name, serviceGetter);
}
// ๐ฏ Get a service
public get<T>(name: string): T {
const serviceGetter = this.services.get(name);
if (!serviceGetter) {
throw new Error(`โ Service '${name}' not found!`);
}
return serviceGetter();
}
// ๐ List registered services
public listServices(): string[] {
return Array.from(this.services.keys());
}
}
// ๐จ Example services
class EmailService {
send(to: string, message: string): void {
console.log(`๐ง Sending email to ${to}: ${message}`);
}
}
class NotificationService {
notify(user: string, message: string): void {
console.log(`๐ Notifying ${user}: ${message}`);
}
}
class CacheService {
private cache: Map<string, any> = new Map();
set(key: string, value: any): void {
this.cache.set(key, value);
console.log(`๐พ Cached: ${key}`);
}
get(key: string): any {
return this.cache.get(key);
}
}
// ๐ฎ Register and use services
const registry = ServiceRegistry.getInstance();
registry.register("email", () => new EmailService());
registry.register("notifications", () => new NotificationService());
registry.register("cache", () => new CacheService());
// ๐ Use services from anywhere
const email1 = registry.get<EmailService>("email");
const email2 = registry.get<EmailService>("email");
console.log("Same email service?", email1 === email2); // true โ
email1.send("[email protected]", "Welcome! ๐");
const notifications = registry.get<NotificationService>("notifications");
notifications.notify("Alice", "New message! ๐ฌ");
const cache = registry.get<CacheService>("cache");
cache.set("user:123", { name: "Bob ๐ป", level: 42 });
console.log("๐ Available services:", registry.listServices());
โ ๏ธ Common Pitfalls and Solutions
Letโs avoid the common mistakes developers make with Singletons! ๐ก๏ธ
โ Wrong: Multiple Instance Creation
// โ BAD: Forgetting to make constructor private
class BadSingleton {
static instance: BadSingleton;
constructor() { // ๐จ Public constructor!
console.log("Creating instance...");
}
static getInstance(): BadSingleton {
if (!this.instance) {
this.instance = new BadSingleton();
}
return this.instance;
}
}
// ๐ฑ This creates multiple instances!
const bad1 = new BadSingleton(); // Oops!
const bad2 = new BadSingleton(); // Double oops!
const bad3 = BadSingleton.getInstance();
console.log(bad1 === bad2); // false โ
โ Correct: Private Constructor
// โ
GOOD: Private constructor prevents direct instantiation
class GoodSingleton {
private static instance: GoodSingleton;
private constructor() { // ๐ Private!
console.log("โจ Singleton created!");
}
public static getInstance(): GoodSingleton {
if (!GoodSingleton.instance) {
GoodSingleton.instance = new GoodSingleton();
}
return GoodSingleton.instance;
}
}
// const wrong = new GoodSingleton(); // ๐ซ TypeScript error!
const good1 = GoodSingleton.getInstance();
const good2 = GoodSingleton.getInstance();
console.log(good1 === good2); // true โ
โ Wrong: Testing Difficulties
// โ BAD: Hard to test singleton
class HardToTestService {
private static instance: HardToTestService;
private constructor() {}
static getInstance(): HardToTestService {
if (!this.instance) {
this.instance = new HardToTestService();
}
return this.instance;
}
fetchData(): string {
// ๐จ Hard-coded dependency!
return "production data";
}
}
โ Correct: Testable Singleton
// โ
GOOD: Testable singleton with reset capability
class TestableSingleton {
private static instance: TestableSingleton;
private dataSource: () => string;
private constructor(dataSource?: () => string) {
this.dataSource = dataSource || (() => "production data");
}
public static getInstance(dataSource?: () => string): TestableSingleton {
if (!TestableSingleton.instance) {
TestableSingleton.instance = new TestableSingleton(dataSource);
}
return TestableSingleton.instance;
}
// ๐งช For testing only
public static resetInstance(): void {
TestableSingleton.instance = null as any;
}
public fetchData(): string {
return this.dataSource();
}
}
// ๐งช In tests
TestableSingleton.resetInstance();
const testInstance = TestableSingleton.getInstance(() => "test data");
console.log(testInstance.fetchData()); // "test data" โ
๐ ๏ธ Best Practices
Here are the golden rules for using Singletons effectively! ๐
1. Use Singletons Sparingly ๐ฏ
// ๐ Good use cases:
// - Application configuration
// - Logger instances
// - Connection pools
// - Cache managers
// ๐ Avoid for:
// - Business logic classes
// - Data models
// - UI components
2. Make It Thread-Safe ๐
class ThreadSafeSingleton {
private static instance: ThreadSafeSingleton;
private static isInitializing = false;
private constructor() {}
public static async getInstance(): Promise<ThreadSafeSingleton> {
if (!ThreadSafeSingleton.instance) {
if (!ThreadSafeSingleton.isInitializing) {
ThreadSafeSingleton.isInitializing = true;
ThreadSafeSingleton.instance = new ThreadSafeSingleton();
ThreadSafeSingleton.isInitializing = false;
} else {
// โณ Wait for initialization
while (ThreadSafeSingleton.isInitializing) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
return ThreadSafeSingleton.instance;
}
}
3. Consider Dependency Injection ๐
// ๐ฏ Better than hard-coded singletons
interface IConfigService {
get(key: string): any;
}
class AppComponent {
constructor(private config: IConfigService) {
// ๐ช Easy to test and swap implementations
}
}
4. Document Singleton Usage ๐
/**
* ๐ Application-wide settings manager (Singleton)
*
* @example
* const settings = SettingsManager.getInstance();
* settings.set('theme', 'dark');
*
* @singleton This class maintains a single instance
*/
class SettingsManager {
// Implementation...
}
5. Provide Cleanup Methods ๐งน
class ResourceManager {
private static instance: ResourceManager;
private resources: any[] = [];
private constructor() {}
public static getInstance(): ResourceManager {
if (!ResourceManager.instance) {
ResourceManager.instance = new ResourceManager();
}
return ResourceManager.instance;
}
// ๐งน Clean up resources
public cleanup(): void {
console.log("๐งน Cleaning up resources...");
this.resources.forEach(resource => resource.dispose?.());
this.resources = [];
}
// ๐ Reset for testing
public static reset(): void {
if (ResourceManager.instance) {
ResourceManager.instance.cleanup();
ResourceManager.instance = null as any;
}
}
}
๐งช Hands-On Exercise
Time to put your singleton skills to the test! ๐ช
๐ฏ Challenge: Build a Multi-Feature App Manager
Create a singleton AppManager
that:
- Manages user sessions
- Tracks app analytics
- Handles feature flags
- Provides a plugin system
Requirements:
- Single instance throughout the app
- Thread-safe initialization
- Plugin registration system
- Analytics event tracking
- Feature flag management
Starter Code:
interface Plugin {
name: string;
init(): void;
}
interface AnalyticsEvent {
name: string;
data?: any;
timestamp: Date;
}
// ๐ฏ Your challenge: Complete this implementation!
class AppManager {
// TODO: Implement singleton pattern
// TODO: Add user session management
// TODO: Add analytics tracking
// TODO: Add feature flags
// TODO: Add plugin system
}
// Test your implementation:
// const app = AppManager.getInstance();
// app.login("user123");
// app.track("page_view", { page: "home" });
// app.isFeatureEnabled("dark_mode");
// app.registerPlugin(myPlugin);
๐ Click to see the solution
interface Plugin {
name: string;
init(): void;
}
interface AnalyticsEvent {
name: string;
data?: any;
timestamp: Date;
}
interface UserSession {
userId: string;
loginTime: Date;
isActive: boolean;
}
class AppManager {
private static instance: AppManager;
private static initPromise: Promise<AppManager>;
private currentSession: UserSession | null = null;
private analytics: AnalyticsEvent[] = [];
private featureFlags: Map<string, boolean> = new Map();
private plugins: Map<string, Plugin> = new Map();
private initialized = false;
private constructor() {}
// ๐ฏ Thread-safe async initialization
private async initialize(): Promise<void> {
if (this.initialized) return;
console.log("๐ Initializing AppManager...");
// ๐ญ Simulate async config loading
await new Promise(resolve => setTimeout(resolve, 100));
// ๐ณ๏ธ Load default feature flags
this.featureFlags.set("dark_mode", true);
this.featureFlags.set("beta_features", false);
this.featureFlags.set("analytics", true);
this.initialized = true;
console.log("โ
AppManager ready!");
}
// ๐ Get singleton instance
public static async getInstance(): Promise<AppManager> {
if (!AppManager.instance) {
if (!AppManager.initPromise) {
AppManager.initPromise = (async () => {
AppManager.instance = new AppManager();
await AppManager.instance.initialize();
return AppManager.instance;
})();
}
return AppManager.initPromise;
}
return AppManager.instance;
}
// ๐ค User session management
public login(userId: string): void {
if (this.currentSession?.isActive) {
this.logout();
}
this.currentSession = {
userId,
loginTime: new Date(),
isActive: true
};
console.log(`๐ค User ${userId} logged in!`);
this.track("user_login", { userId });
}
public logout(): void {
if (this.currentSession) {
const sessionDuration = Date.now() - this.currentSession.loginTime.getTime();
this.track("user_logout", {
userId: this.currentSession.userId,
sessionDuration
});
console.log(`๐ User ${this.currentSession.userId} logged out!`);
this.currentSession = null;
}
}
public getCurrentUser(): string | null {
return this.currentSession?.userId || null;
}
// ๐ Analytics tracking
public track(eventName: string, data?: any): void {
if (!this.isFeatureEnabled("analytics")) {
return;
}
const event: AnalyticsEvent = {
name: eventName,
data,
timestamp: new Date()
};
this.analytics.push(event);
console.log(`๐ Tracked: ${eventName}`, data || "");
// ๐งน Keep only last 1000 events
if (this.analytics.length > 1000) {
this.analytics = this.analytics.slice(-1000);
}
}
public getAnalytics(): AnalyticsEvent[] {
return [...this.analytics];
}
// ๐ณ๏ธ Feature flags
public isFeatureEnabled(feature: string): boolean {
return this.featureFlags.get(feature) || false;
}
public setFeatureFlag(feature: string, enabled: boolean): void {
this.featureFlags.set(feature, enabled);
console.log(`๐ณ๏ธ Feature '${feature}' ${enabled ? "enabled" : "disabled"}`);
this.track("feature_flag_changed", { feature, enabled });
}
// ๐ Plugin system
public registerPlugin(plugin: Plugin): void {
if (this.plugins.has(plugin.name)) {
console.warn(`โ ๏ธ Plugin '${plugin.name}' already registered!`);
return;
}
this.plugins.set(plugin.name, plugin);
plugin.init();
console.log(`๐ Plugin '${plugin.name}' registered!`);
this.track("plugin_registered", { pluginName: plugin.name });
}
public getPlugin(name: string): Plugin | undefined {
return this.plugins.get(name);
}
// ๐ App statistics
public getStats(): any {
return {
currentUser: this.getCurrentUser(),
totalEvents: this.analytics.length,
activeFeatures: Array.from(this.featureFlags.entries())
.filter(([_, enabled]) => enabled)
.map(([feature]) => feature),
loadedPlugins: Array.from(this.plugins.keys()),
sessionActive: this.currentSession?.isActive || false
};
}
}
// ๐งช Test the implementation
async function testAppManager() {
// ๐ฏ Get instance
const app1 = await AppManager.getInstance();
const app2 = await AppManager.getInstance();
console.log("Same instance?", app1 === app2); // true โ
// ๐ค User management
app1.login("alice_123");
console.log("Current user:", app1.getCurrentUser());
// ๐ Analytics
app1.track("page_view", { page: "dashboard" });
app1.track("button_click", { button: "save" });
// ๐ณ๏ธ Feature flags
console.log("Dark mode enabled?", app1.isFeatureEnabled("dark_mode"));
app1.setFeatureFlag("beta_features", true);
// ๐ Plugins
const chatPlugin: Plugin = {
name: "chat",
init() {
console.log("๐ฌ Chat plugin initialized!");
}
};
const mapPlugin: Plugin = {
name: "maps",
init() {
console.log("๐บ๏ธ Maps plugin initialized!");
}
};
app1.registerPlugin(chatPlugin);
app2.registerPlugin(mapPlugin); // Same instance!
// ๐ Stats
console.log("๐ App Stats:", app1.getStats());
// ๐ Logout
app1.logout();
}
// ๐ Run the test
testAppManager();
๐ Bonus Challenge
Extend the AppManager
with:
- Notification system with subscription
- Cache manager with TTL (time-to-live)
- Error tracking with stack traces
- Performance monitoring
๐ Key Takeaways
Youโve mastered the Singleton pattern! Hereโs what youโve learned: ๐
โ
Singleton ensures only ONE instance exists throughout your app
โ
Private constructors prevent direct instantiation
โ
Static getInstance() provides global access point
โ
Thread-safe implementations handle concurrent access
โ
Real-world uses include config, logging, and connection pools
Remember: With great power comes great responsibility! Use singletons wisely โ theyโre powerful but can make testing harder if overused. ๐ท๏ธ
๐ค Next Steps
Congratulations on mastering the Singleton pattern! ๐ Hereโs how to continue your design patterns journey:
- ๐ญ Explore Factory Pattern - Learn how to create objects without specifying their exact class
- ๐จ Try Builder Pattern - Master complex object construction step by step
- ๐ญ Study Observer Pattern - Implement event-driven architectures
- ๐งฉ Practice Adapter Pattern - Make incompatible interfaces work together
Keep coding, keep learning, and remember โ youโre building amazing things! ๐โจ
Happy Singleton-ing! ๐ช๐ฏ