Proxy Pattern
Overview
A gallery may postpone loading an image until it is actually displayed. Instead of adding a loading check to every caller, provide a substitute that can be used like an image.
Proxy accepts requests through the real object contract and controls access to it. This example creates the real object only on the first draw.
UML
RealImage and ImageProxy implement IImage. ImageProxy holds Lazy<RealImage> and delegates Draw() to the real object.
History
GoF cataloged Proxy as a structural pattern in Design Patterns in 1994. A substitute or placeholder controls access to another object.1
Why use it?
Callers use the real image and its proxy in the same way. The proxy owns the decision to defer creation.
An authorization proxy must check current permission on each request. This example covers deferred creation only; it supplies neither authentication nor remote communication.
Example
Compare the real-object count before and after the first draw. The stand-in counts construction instead of opening files.
using System;
using System.Collections.Generic;
using System.Linq;
IImage image = new ImageProxy();
Console.WriteLine(RealImage.Created);
Console.WriteLine(image.Draw());
Console.WriteLine(RealImage.Created);
// Let callers use the real subject and its proxy through the same contract.
interface IImage { string Draw(); }
sealed class RealImage : IImage
{
public static int Created { get; private set; }
public RealImage() => Created++;
public string Draw() => "draw image";
}
// Control construction time and delegate the actual work to the created object.
sealed class ImageProxy : IImage
{
private readonly Lazy<RealImage> image = new(() => new RealImage());
public string Draw() => image.Value.Draw();
}
0
draw image
1Constructing the proxy does not construct RealImage. The first access to Lazy.Value creates it.
An extended example
Draw twice through the same proxy and check that the real object was created only once.
using System;
using System.Collections.Generic;
using System.Linq;
IImage image = new ImageProxy();
Console.WriteLine(image.Draw());
Console.WriteLine(image.Draw());
Console.WriteLine("created: " + RealImage.Created);
// Let callers use the real subject and its proxy through the same contract.
interface IImage { string Draw(); }
sealed class RealImage : IImage
{
public static int Created { get; private set; }
public RealImage() => Created++;
public string Draw() => "draw image";
}
// Control construction time and delegate the actual work to the created object.
sealed class ImageProxy : IImage
{
private readonly Lazy<RealImage> image = new(() => new RealImage());
public string Draw() => image.Value.Draw();
}
draw image
draw image
created: 1Lazy reuses the object, not the result of Draw(). Each draw still calls the real method. A real file or GPU resource also needs an explicit disposal owner.
TypeScript example
Callers draw through IImage, while the proxy decides when to create the real object. Keeping creation control separate prevents every screen from repeating the lazy-loading check.
// Let callers use the real subject and its proxy through the same contract.
interface IImage { draw(): string; }
class RealImage implements IImage {
static created = 0;
constructor() { RealImage.created++; }
draw(): string { return "draw image"; }
}
// Control construction time and delegate the actual work to the created object.
class ImageProxy implements IImage {
#image: RealImage | undefined;
draw(): string {
this.#image ??= new RealImage();
return this.#image.draw();
}
}
const image: IImage = new ImageProxy();
console.log(RealImage.created);
console.log(image.draw());
console.log(image.draw());
console.log("created: " + RealImage.created);0
draw image
draw image
created: 1The #image field expresses the deferred creation performed by C# Lazy<RealImage>. It reuses the object rather than caching draw() results, and reads no image files.
When it becomes an anti-pattern
A proxy that hides file access, network calls or retries changes latency and failure behavior behind a familiar method. Inserting remote work while callers still expect a local calculation makes the apparent transparency conceal failures. Real I/O needs explicit cancellation, failure and execution-count contracts.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Structural patterns, Proxy. β©