Loading learning content...
In the previous pages, we learned how individual objects can clone themselves. But in real systems, you need to manage collections of prototypes—retrieving the right prototype by name, registering new prototypes at runtime, and providing a unified interface for object creation.\n\nThis is where prototype registries come in. A registry acts as a centralized catalog of prototypes, allowing clients to request copies of specific prototype types without knowing their concrete classes or even their existence at compile time.\n\nPrototype registries are particularly valuable in plugin systems, user-defined templates, configuration-driven architectures, and any scenario where the set of available object types is determined at runtime.
By the end of this page, you will understand how to design and implement prototype registries, integrate them with factory patterns, support runtime extensibility through plugins and configuration, and apply these patterns to real-world systems.
Without a registry, using the Prototype Pattern requires directly holding references to prototype objects. This works for simple cases but becomes problematic as systems grow:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// WITHOUT a registry: scattered prototype references interface Shape { clone(): Shape; draw(): void;} class Circle implements Shape { constructor(public radius: number, public color: string) {} clone(): Circle { return new Circle(this.radius, this.color); } draw(): void { console.log(`Drawing ${this.color} circle, r=${this.radius}`); }} class Rectangle implements Shape { constructor(public width: number, public height: number, public color: string) {} clone(): Rectangle { return new Rectangle(this.width, this.height, this.color); } draw(): void { console.log(`Drawing ${this.color} rectangle`); }} // Problem 1: Must maintain individual prototype referencesconst redCirclePrototype = new Circle(50, "red");const blueCirclePrototype = new Circle(30, "blue");const greenRectPrototype = new Rectangle(100, 50, "green"); // Problem 2: Client code needs to know about all prototypesfunction createShape(type: string): Shape | null { // This function must know about every prototype! switch (type) { case "red-circle": return redCirclePrototype.clone(); case "blue-circle": return blueCirclePrototype.clone(); case "green-rect": return greenRectPrototype.clone(); default: return null; }} // Problem 3: Adding new types requires modifying factory code// What if a plugin wants to register a "star" shape?// What if user creates a custom "my-special-circle" prototype? // Problem 4: Prototypes are scattered across the codebase// Where is the single source of truth for available shapes? // Problem 5: No introspection// How do we list all available shape types?// How do we check if a type exists before cloning?A prototype registry is essentially a map from identifiers to prototype instances, combined with methods for registration, lookup, and cloning. Let's build a complete, production-quality implementation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
// Production-quality Prototype Registry interface Prototype<T> { clone(): T;} class PrototypeRegistry<T extends Prototype<T>> { private readonly prototypes: Map<string, T> = new Map(); /** * Register a prototype with a unique key. * @throws Error if key already exists (use replace to update) */ register(key: string, prototype: T): void { if (this.prototypes.has(key)) { throw new Error( `Prototype with key "${key}" already registered. ` + `Use replace() to update or unregister() first.` ); } this.prototypes.set(key, prototype); } /** * Replace an existing prototype or register new. * Returns the previously registered prototype, if any. */ replace(key: string, prototype: T): T | undefined { const previous = this.prototypes.get(key); this.prototypes.set(key, prototype); return previous; } /** * Remove a prototype from the registry. * Returns true if removed, false if not found. */ unregister(key: string): boolean { return this.prototypes.delete(key); } /** * Check if a prototype exists. */ has(key: string): boolean { return this.prototypes.has(key); } /** * Get a CLONE of the registered prototype. * Returns undefined if not found. */ create(key: string): T | undefined { const prototype = this.prototypes.get(key); return prototype?.clone(); } /** * Get a clone or throw if not found. */ createOrThrow(key: string): T { const prototype = this.prototypes.get(key); if (!prototype) { throw new Error( `No prototype registered with key "${key}". ` + `Available: [${this.keys().join(", ")}]` ); } return prototype.clone(); } /** * Get direct reference to prototype (for inspection, not for use). * WARNING: Do not modify the returned prototype! */ getPrototype(key: string): T | undefined { return this.prototypes.get(key); } /** * List all registered keys. */ keys(): string[] { return Array.from(this.prototypes.keys()); } /** * Get count of registered prototypes. */ get size(): number { return this.prototypes.size; } /** * Clear all prototypes. */ clear(): void { this.prototypes.clear(); } /** * Register multiple prototypes at once. */ registerAll(prototypes: Record<string, T>): void { for (const [key, prototype] of Object.entries(prototypes)) { this.register(key, prototype); } }} // ============================================// Usage Example: Shape Registry// ============================================ interface Shape extends Prototype<Shape> { draw(): void; getType(): string;} class Circle implements Shape { constructor( private radius: number, private color: string ) {} clone(): Circle { return new Circle(this.radius, this.color); } draw(): void { console.log(`Drawing ${this.color} circle, radius ${this.radius}`); } getType(): string { return "circle"; }} class Rectangle implements Shape { constructor( private width: number, private height: number, private color: string ) {} clone(): Rectangle { return new Rectangle(this.width, this.height, this.color); } draw(): void { console.log(`Drawing ${this.color} rectangle ${this.width}x${this.height}`); } getType(): string { return "rectangle"; }} // Create and populate registryconst shapeRegistry = new PrototypeRegistry<Shape>(); shapeRegistry.registerAll({ "small-red-circle": new Circle(25, "red"), "large-blue-circle": new Circle(100, "blue"), "standard-button": new Rectangle(150, 40, "gray"), "wide-banner": new Rectangle(800, 100, "navy"),}); // Client code uses string keys - no type knowledge neededconst shape1 = shapeRegistry.createOrThrow("small-red-circle");const shape2 = shapeRegistry.createOrThrow("standard-button"); shape1.draw(); // "Drawing red circle, radius 25"shape2.draw(); // "Drawing gray rectangle 150x40" // List available shapesconsole.log("Available shapes:", shapeRegistry.keys());// ["small-red-circle", "large-blue-circle", "standard-button", "wide-banner"] // Check existence before creatingif (shapeRegistry.has("custom-shape")) { const custom = shapeRegistry.create("custom-shape");}The registry's create() method MUST return clones, never the stored prototypes directly. If clients receive references to stored prototypes, they might accidentally modify them, affecting all future clones. The registry protects its prototypes.
Complex applications often need multiple levels of prototypes: global defaults, module-specific overrides, and user customizations. Hierarchical registries support this through parent-child relationships, where child registries can override parent prototypes while inheriting everything else.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
// Hierarchical Registry with parent chain lookup interface Prototype<T> { clone(): T;} class HierarchicalPrototypeRegistry<T extends Prototype<T>> { private readonly prototypes: Map<string, T> = new Map(); private readonly parent: HierarchicalPrototypeRegistry<T> | null; constructor(parent: HierarchicalPrototypeRegistry<T> | null = null) { this.parent = parent; } /** * Create a child registry that inherits from this registry. */ createChild(): HierarchicalPrototypeRegistry<T> { return new HierarchicalPrototypeRegistry(this); } /** * Register a prototype at this level. */ register(key: string, prototype: T): void { this.prototypes.set(key, prototype); } /** * Check if key exists at this level (not inherited). */ hasOwn(key: string): boolean { return this.prototypes.has(key); } /** * Check if key exists anywhere in the hierarchy. */ has(key: string): boolean { if (this.prototypes.has(key)) return true; if (this.parent) return this.parent.has(key); return false; } /** * Create a clone, walking up the hierarchy to find the prototype. */ create(key: string): T | undefined { // First, check this registry const local = this.prototypes.get(key); if (local) return local.clone(); // Then, check parent chain if (this.parent) return this.parent.create(key); return undefined; } /** * Get all keys available, including inherited ones. */ allKeys(): string[] { const keys = new Set<string>(this.prototypes.keys()); if (this.parent) { for (const key of this.parent.allKeys()) { keys.add(key); // Set handles duplicates } } return Array.from(keys); } /** * Get keys defined only at this level. */ ownKeys(): string[] { return Array.from(this.prototypes.keys()); } /** * Remove from this level only. */ unregister(key: string): boolean { return this.prototypes.delete(key); }} // ============================================// Usage: Application with Global, Module, and User Levels// ============================================ interface Button extends Prototype<Button> { render(): string;} class StandardButton implements Button { constructor( private label: string, private color: string, private size: string ) {} clone(): StandardButton { return new StandardButton(this.label, this.color, this.size); } render(): string { return `[${this.size} ${this.color} button: ${this.label}]`; }} // LEVEL 1: Global defaults (application-wide)const globalRegistry = new HierarchicalPrototypeRegistry<Button>();globalRegistry.register("primary", new StandardButton("Click", "blue", "medium"));globalRegistry.register("secondary", new StandardButton("Cancel", "gray", "medium"));globalRegistry.register("danger", new StandardButton("Delete", "red", "medium")); // LEVEL 2: Module-specific registry (inherits global, adds/overrides)const adminModuleRegistry = globalRegistry.createChild();adminModuleRegistry.register("danger", new StandardButton("CONFIRM DELETE", "darkred", "large"));adminModuleRegistry.register("admin-action", new StandardButton("Admin Only", "purple", "medium")); // LEVEL 3: User-specific registry (inherits module, adds overrides)const userRegistry = adminModuleRegistry.createChild();userRegistry.register("primary", new StandardButton("Click Me!", "green", "small")); // Usage from different contexts: // Global context gets global defaultsconst globalBtn = globalRegistry.create("primary")!;console.log(globalBtn.render()); // [medium blue button: Click] // Admin module gets overridden danger buttonconst adminDanger = adminModuleRegistry.create("danger")!;console.log(adminDanger.render()); // [large darkred button: CONFIRM DELETE] // Admin module still inherits non-overridden buttonsconst adminSecondary = adminModuleRegistry.create("secondary")!;console.log(adminSecondary.render()); // [medium gray button: Cancel] // User gets their custom primaryconst userPrimary = userRegistry.create("primary")!;console.log(userPrimary.render()); // [small green button: Click Me!] // User inherits admin's danger (which overrode global)const userDanger = userRegistry.create("danger")!;console.log(userDanger.render()); // [large darkred button: CONFIRM DELETE] // User inherits admin-only buttonconst userAdmin = userRegistry.create("admin-action")!;console.log(userAdmin.render()); // [medium purple button: Admin Only] // Introspectionconsole.log("User's all keys:", userRegistry.allKeys());// ["primary", "danger", "admin-action", "secondary"] console.log("User's own keys:", userRegistry.ownKeys());// ["primary"]Hierarchical registries mirror patterns seen in CSS cascading, configuration inheritance (environment variables overriding defaults), and scope chains in programming languages. They're natural when you have layered customization requirements.
In large systems, eagerly loading all prototypes at startup can be expensive. Lazy registries defer prototype creation until first access, and dynamic registration allows runtime prototype addition from configuration, plugins, or user actions.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
// Lazy-loading Prototype Registry interface Prototype<T> { clone(): T;} // Factory functions create prototypes on demandtype PrototypeFactory<T> = () => T; class LazyPrototypeRegistry<T extends Prototype<T>> { // Eagerly loaded prototypes private readonly prototypes: Map<string, T> = new Map(); // Lazy factories (not yet instantiated) private readonly factories: Map<string, PrototypeFactory<T>> = new Map(); /** * Register an already-created prototype. */ register(key: string, prototype: T): void { this.factories.delete(key); // Remove factory if exists this.prototypes.set(key, prototype); } /** * Register a factory function for lazy instantiation. * Prototype will be created on first access. */ registerLazy(key: string, factory: PrototypeFactory<T>): void { this.prototypes.delete(key); // Remove instance if exists this.factories.set(key, factory); } /** * Get or create prototype, then clone. */ create(key: string): T | undefined { // Check if already instantiated let prototype = this.prototypes.get(key); if (!prototype) { // Try to instantiate from factory const factory = this.factories.get(key); if (factory) { console.log(`Lazily instantiating prototype: ${key}`); prototype = factory(); this.prototypes.set(key, prototype); this.factories.delete(key); // Factory no longer needed } } return prototype?.clone(); } /** * Check if key exists (either as prototype or factory). */ has(key: string): boolean { return this.prototypes.has(key) || this.factories.has(key); } /** * Get all registered keys (both loaded and lazy). */ keys(): string[] { const allKeys = new Set([ ...this.prototypes.keys(), ...this.factories.keys() ]); return Array.from(allKeys); } /** * Check what's actually loaded vs pending. */ stats(): { loaded: number; pending: number } { return { loaded: this.prototypes.size, pending: this.factories.size }; } /** * Force instantiation of all lazy prototypes. */ preloadAll(): void { for (const [key, factory] of this.factories) { this.prototypes.set(key, factory()); } this.factories.clear(); }} // ============================================// Usage: Lazy Loading Game Enemies// ============================================ interface Enemy extends Prototype<Enemy> { attack(): void; name: string;} class Goblin implements Enemy { name = "Goblin"; constructor( private health: number, private damage: number, private texture: string // Expensive to load! ) { console.log("Loading Goblin texture: " + texture); } clone(): Goblin { return new Goblin(this.health, this.damage, this.texture); } attack(): void { console.log(`Goblin attacks for ${this.damage} damage!`); }} class Dragon implements Enemy { name = "Dragon"; constructor( private health: number, private damage: number, private fireBreath: boolean, private animations: string[] // Very expensive! ) { console.log("Loading Dragon with " + animations.length + " animations"); } clone(): Dragon { return new Dragon(this.health, this.damage, this.fireBreath, [...this.animations]); } attack(): void { console.log(`Dragon attacks for ${this.damage} damage!`); }} // Create registry with lazy factoriesconst enemyRegistry = new LazyPrototypeRegistry<Enemy>(); // Register factories - NO work done yet!enemyRegistry.registerLazy("goblin", () => new Goblin(30, 5, "textures/goblin_base.png")); enemyRegistry.registerLazy("dragon", () => new Dragon(500, 50, true, [ "animations/dragon_fly.anim", "animations/dragon_attack.anim", "animations/dragon_breathe.anim", ])); console.log(enemyRegistry.stats()); // { loaded: 0, pending: 2 }console.log("Available enemies:", enemyRegistry.keys()); // ["goblin", "dragon"] // Player enters goblin area - NOW we load goblinconst goblin = enemyRegistry.create("goblin");// Output: "Loading Goblin texture: textures/goblin_base.png"// "Lazily instantiating prototype: goblin" console.log(enemyRegistry.stats()); // { loaded: 1, pending: 1 } // Create more goblins - uses cached prototype (no loading)const goblin2 = enemyRegistry.create("goblin"); // Silent - already loaded // Dragon never loaded if player doesn't reach dragon area!console.log(enemyRegistry.stats()); // Still { loaded: 1, pending: 1 }Lazy loading is especially valuable when prototypes require expensive initialization (loading assets, database queries, network calls). Register factories for prototypes that might not be needed, especially in large applications with many features.
Truly flexible systems define prototypes through configuration files or databases rather than code. This enables non-developers (designers, content creators, operations teams) to manage prototypes without deploying new code.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
// Configuration-driven Prototype Registry // Prototype definition from config fileinterface PrototypeConfig { type: string; // Maps to a factory properties: Record<string, any>;} // Registry config (loaded from JSON/YAML/database)interface RegistryConfig { prototypes: Record<string, PrototypeConfig>;} // Factory interface for creating prototypes from configinterface PrototypeFactory<T> { create(properties: Record<string, any>): T;} interface Prototype<T> { clone(): T;} class ConfigDrivenRegistry<T extends Prototype<T>> { private readonly prototypes: Map<string, T> = new Map(); private readonly factories: Map<string, PrototypeFactory<T>> = new Map(); /** * Register a factory for a prototype type. * Factories know how to create prototypes from config properties. */ registerFactory(type: string, factory: PrototypeFactory<T>): void { this.factories.set(type, factory); } /** * Load prototypes from configuration. */ loadFromConfig(config: RegistryConfig): void { for (const [key, protoConfig] of Object.entries(config.prototypes)) { const factory = this.factories.get(protoConfig.type); if (!factory) { console.warn( `Unknown prototype type "${protoConfig.type}" for key "${key}". ` + `Available types: [${Array.from(this.factories.keys()).join(", ")}]` ); continue; } try { const prototype = factory.create(protoConfig.properties); this.prototypes.set(key, prototype); console.log(`Loaded prototype: ${key} (type: ${protoConfig.type})`); } catch (error) { console.error(`Failed to create prototype "${key}":`, error);} } } create(key: string): T | undefined { return this.prototypes.get(key)?.clone();} keys(): string[] { return Array.from(this.prototypes.keys());}} // ============================================// Example: Notification Templates// ============================================ interface Notification extends Prototype<Notification> { send(recipient: string, data: Record<string, string>): void;} class EmailNotification implements Notification { constructor( private subject: string, private bodyTemplate: string, private fromAddress: string ) { } clone(): EmailNotification { return new EmailNotification(this.subject, this.bodyTemplate, this.fromAddress); } send(recipient: string, data: Record<string, string>): void { let body = this.bodyTemplate; for (const [key, value] of Object.entries(data)) { body = body.replace(`{{${key}}}`, value); } console.log(`Email to ${recipient}:`); console.log(` From: ${this.fromAddress}`); console.log(` Subject: ${this.subject}`); console.log(` Body: ${body}`); }} class SMSNotification implements Notification { constructor( private messageTemplate: string, private fromNumber: string ) {} clone(): SMSNotification { return new SMSNotification(this.messageTemplate, this.fromNumber); } send(recipient: string, data: Record<string, string>): void { let message = this.messageTemplate; for (const [key, value] of Object.entries(data)) { message = message.replace(`{{${key}}}`, value); } console.log(`SMS to ${recipient} from ${this.fromNumber}: ${message}`); }} // Register factoriesconst notificationRegistry = new ConfigDrivenRegistry<Notification>(); notificationRegistry.registerFactory("email", { create(props) { return new EmailNotification( props.subject, props.bodyTemplate, props.fromAddress ); }}); notificationRegistry.registerFactory("sms", { create(props) { return new SMSNotification( props.messageTemplate, props.fromNumber ); }}); // Configuration loaded from file (in reality: JSON/YAML/database)const config: RegistryConfig = { prototypes: { "welcome-email": { type: "email", properties: { subject: "Welcome to Our Platform, {{userName}}!", bodyTemplate: "Hello {{userName}}, thank you for joining.", fromAddress: "hello@example.com" } }, "password-reset-email": { type: "email", properties: { subject: "Password Reset Request", bodyTemplate: "Click here to reset: {{resetLink}}", fromAddress: "security@example.com" } }, "order-sms": { type: "sms", properties: { messageTemplate: "Order {{orderId}} shipped! Track: {{trackingUrl}}", fromNumber: "+1-555-0123" } } }}; // Load from confignotificationRegistry.loadFromConfig(config); // Use notifications - marketing team can add new templates without code changes!const welcome = notificationRegistry.create("welcome-email")!;welcome.send("user@test.com", { userName: "Alice" }); const orderSms = notificationRegistry.create("order-sms")!;orderSms.send("+1-555-9876", { orderId: "12345", trackingUrl: "track.example.com/12345" });Prototype registries are essential for plugin architectures where third-party code can extend the system with new prototype types. The core application defines the registry interface and base prototypes; plugins register additional prototypes at runtime.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
// Plugin Architecture with Prototype Registry // ============================================// CORE APPLICATION (provided by app vendor)// ============================================ interface Prototype<T> { clone(): T;} interface Widget extends Prototype<Widget> { readonly type: string; render(): string; configure(options: Record<string, any>): void;} // Global widget registry exposed to pluginsclass WidgetRegistry { private static instance: WidgetRegistry; private readonly widgets: Map<string, Widget> = new Map(); static getInstance(): WidgetRegistry { if (!WidgetRegistry.instance) { WidgetRegistry.instance = new WidgetRegistry(); } return WidgetRegistry.instance; } register(key: string, widget: Widget): void { if (this.widgets.has(key)) { console.warn(`Widget "${key}" already registered, overwriting.`); } this.widgets.set(key, widget); console.log(`Registered widget: ${key}`); } create(key: string): Widget | undefined { return this.widgets.get(key)?.clone(); } getAvailableWidgets(): string[] { return Array.from(this.widgets.keys()); }} // Plugin interface definitioninterface Plugin { name: string; version: string; initialize(registry: WidgetRegistry): void;} // Application plugin loaderclass PluginLoader { private readonly loadedPlugins: Plugin[] = []; private readonly registry: WidgetRegistry; constructor(registry: WidgetRegistry) { this.registry = registry; } loadPlugin(plugin: Plugin): void { console.log(`Loading plugin: ${plugin.name} v${plugin.version}`); plugin.initialize(this.registry); this.loadedPlugins.push(plugin); console.log(`Plugin ${plugin.name} loaded successfully.\n`); } getLoadedPlugins(): string[] { return this.loadedPlugins.map(p => `${p.name} v${p.version}`); }} // Built-in widgets (core application)class TextWidget implements Widget { readonly type = "text"; private content: string = ""; constructor(private fontSize: number = 14) {} clone(): TextWidget { const copy = new TextWidget(this.fontSize); copy.content = this.content; return copy; } render(): string { return `<div style="font-size: ${this.fontSize}px">${this.content}</div>`; } configure(options: Record<string, any>): void { if (options.content) this.content = options.content; if (options.fontSize) this.fontSize = options.fontSize; }} class ImageWidget implements Widget { readonly type = "image"; private src: string = ""; private alt: string = ""; clone(): ImageWidget { const copy = new ImageWidget(); copy.src = this.src; copy.alt = this.alt; return copy; } render(): string { return `<img src="${this.src}" alt="${this.alt}" />`; } configure(options: Record<string, any>): void { if (options.src) this.src = options.src; if (options.alt) this.alt = options.alt; }} // ============================================// PLUGINS (provided by third parties)// ============================================ // Plugin: Chart Widgetsconst chartPlugin: Plugin = { name: "Chart Widgets", version: "1.2.0", initialize(registry: WidgetRegistry): void { class BarChartWidget implements Widget { readonly type = "bar-chart"; private data: number[] = []; private labels: string[] = []; clone(): BarChartWidget { const copy = new BarChartWidget(); copy.data = [...this.data]; copy.labels = [...this.labels]; return copy; } render(): string { return `<div class="bar-chart">Chart: ${this.labels.join(", ")}</div>`; } configure(options: Record<string, any>): void { if (options.data) this.data = options.data; if (options.labels) this.labels = options.labels; } } class PieChartWidget implements Widget { readonly type = "pie-chart"; private segments: { label: string; value: number }[] = []; clone(): PieChartWidget { const copy = new PieChartWidget(); copy.segments = this.segments.map(s => ({ ...s })); return copy; } render(): string { return `<div class="pie-chart">Pie: ${this.segments.length} segments</div>`; } configure(options: Record<string, any>): void { if (options.segments) this.segments = options.segments; } } registry.register("bar-chart", new BarChartWidget()); registry.register("pie-chart", new PieChartWidget()); }}; // Plugin: Social Widgetsconst socialPlugin: Plugin = { name: "Social Media Widgets", version: "2.0.1", initialize(registry: WidgetRegistry): void { class TwitterFeedWidget implements Widget { readonly type = "twitter-feed"; private username: string = ""; private tweetCount: number = 5; clone(): TwitterFeedWidget { const copy = new TwitterFeedWidget(); copy.username = this.username; copy.tweetCount = this.tweetCount; return copy; } render(): string { return `<div class="twitter-feed">@${this.username}'s last ${this.tweetCount} tweets</div>`; } configure(options: Record<string, any>): void { if (options.username) this.username = options.username; if (options.tweetCount) this.tweetCount = options.tweetCount; } } registry.register("twitter-feed", new TwitterFeedWidget()); }}; // ============================================// APPLICATION STARTUP// ============================================ // Initialize registry with core widgetsconst registry = WidgetRegistry.getInstance();registry.register("text", new TextWidget());registry.register("image", new ImageWidget()); // Load pluginsconst loader = new PluginLoader(registry);loader.loadPlugin(chartPlugin);loader.loadPlugin(socialPlugin); // See all available widgetsconsole.log("Available widgets:", registry.getAvailableWidgets());// ["text", "image", "bar-chart", "pie-chart", "twitter-feed"] // Create widgets from registryconst chart = registry.create("bar-chart")!;chart.configure({ labels: ["Q1", "Q2", "Q3", "Q4"], data: [100, 150, 200, 180] });console.log(chart.render()); const feed = registry.create("twitter-feed")!;feed.configure({ username: "nodejs", tweetCount: 3 });console.log(feed.render());When accepting plugins, consider: (1) Namespacing plugin-registered keys to prevent conflicts, (2) Validating prototypes before registration, (3) Sandboxing plugin code execution, and (4) Providing versioning for plugin compatibility.
In multi-threaded applications, prototype registries must be thread-safe. Multiple threads may simultaneously read from and write to the registry. The key considerations are:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
// Thread-safe Prototype Registry in Java import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock; public class ThreadSafePrototypeRegistry<T extends Prototype<T>> { // ConcurrentHashMap for basic thread-safe read/write private final ConcurrentHashMap<String, T> prototypes = new ConcurrentHashMap<>(); // Lazy factories need additional synchronization private final ConcurrentHashMap<String, Supplier<T>> factories = new ConcurrentHashMap<>(); // Lock for lazy initialization to prevent double-creation private final ReadWriteLock lazyLock = new ReentrantReadWriteLock(); /** * Register an eagerly created prototype. */ public void register(String key, T prototype) { // ConcurrentHashMap.put is thread-safe prototypes.put(key, prototype); factories.remove(key); // Remove any lazy factory } /** * Register a lazy factory. */ public void registerLazy(String key, Supplier<T> factory) { prototypes.remove(key); // Remove any eager prototype factories.put(key, factory); } /** * Thread-safe create with lazy initialization handling. */ public Optional<T> create(String key) { // Fast path: check if already instantiated T prototype = prototypes.get(key); if (prototype != null) { return Optional.of(prototype.clone()); } // Check if lazy factory exists Supplier<T> factory = factories.get(key); if (factory == null) { return Optional.empty(); } // Lazy initialization with double-checked locking lazyLock.readLock().lock(); try { prototype = prototypes.get(key); if (prototype != null) { return Optional.of(prototype.clone()); } } finally { lazyLock.readLock().unlock(); } // Upgrade to write lock for instantiation lazyLock.writeLock().lock(); try { // Double-check after acquiring write lock prototype = prototypes.get(key); if (prototype != null) { return Optional.of(prototype.clone()); } // Actually instantiate prototype = factory.get(); prototypes.put(key, prototype); factories.remove(key); return Optional.of(prototype.clone()); } finally { lazyLock.writeLock().unlock(); } } /** * Get snapshot of keys (iteration-safe). */ public Set<String> keys() { Set<String> allKeys = new HashSet<>(); allKeys.addAll(prototypes.keySet()); allKeys.addAll(factories.keySet()); return Collections.unmodifiableSet(allKeys); }} // Usage in multi-threaded contextExecutorService executor = Executors.newFixedThreadPool(10);ThreadSafePrototypeRegistry<Widget> registry = new ThreadSafePrototypeRegistry<>(); registry.register("button", new ButtonWidget());registry.registerLazy("chart", () -> { System.out.println("Creating chart prototype in thread: " + Thread.currentThread().getName()); return new ChartWidget();}); // Multiple threads accessing registry concurrentlyfor (int i = 0; i < 20; i++) { executor.submit(() -> { // Safe: multiple threads can create concurrently Widget button = registry.create("button").orElseThrow(); button.configure(Map.of("label", Thread.currentThread().getName())); // Safe: lazy factory called only once despite concurrent access Widget chart = registry.create("chart").orElseThrow(); });}Prototype registries transform the Prototype Pattern from a single-object technique into a scalable architecture for runtime-configurable object creation:
Module Complete:\n\nYou've now mastered the Prototype Pattern from problem identification through advanced registry implementation. The pattern provides a powerful alternative to factory-based object creation, excelling when:\n\n- Object initialization is expensive\n- Concrete types are unknown at compile time\n- Complex configurations should be preserved as templates\n- Runtime extensibility is required\n\nCombined with registries, lazy loading, and configuration integration, the Prototype Pattern scales from simple object copying to sophisticated, plugin-friendly architectures.
Congratulations! You've completed the Prototype Pattern module. You now understand when, why, and how to implement object cloning—from basic clone methods through deep copying semantics to enterprise-scale prototype registries.