Design Pattern
Structurels

Proxy

Contrôler l'accès à un objet

Problème

On veut contrôler l'accès à un objet : pour du cache, du lazy loading, des vérifications d'autorisation, ou du logging sans modifier l'objet lui-même.

Proxy de cache

interface ProductRepository {
  findById(id: string): Promise<Product | null>;
  findAll(): Promise<Product[]>;
}

class CachingProxy implements ProductRepository {
  private cache = new Map<string, Product>();
  private allProductsCache: Product[] | null = null;

  constructor(private target: ProductRepository) {}

  async findById(id: string): Promise<Product | null> {
    if (this.cache.has(id)) return this.cache.get(id)!;

    const product = await this.target.findById(id);
    if (product) this.cache.set(id, product);
    return product;
  }

  async findAll(): Promise<Product[]> {
    if (this.allProductsCache) return this.allProductsCache;

    const products = await this.target.findAll();
    this.allProductsCache = products;
    products.forEach(p => this.cache.set(p.id, p));
    return products;
  }
}

Proxy d'autorisation

interface AdminRepository {
  deleteUser(id: string): Promise<void>;
  exportAllData(): Promise<Blob>;
  updatePermissions(userId: string, permissions: string[]): Promise<void>;
}

class AuthorizationProxy implements AdminRepository {
  constructor(
    private target: AdminRepository,
    private currentUser: { roles: string[] }
  ) {}

  private assertAdmin() {
    if (!this.currentUser.roles.includes('admin')) {
      throw new Error('Access denied: admin role required');
    }
  }

  async deleteUser(id: string) {
    this.assertAdmin();
    return this.target.deleteUser(id);
  }

  async exportAllData() {
    this.assertAdmin();
    return this.target.exportAllData();
  }

  async updatePermissions(userId: string, permissions: string[]) {
    this.assertAdmin();
    return this.target.updatePermissions(userId, permissions);
  }
}

Proxy JavaScript natif

JavaScript expose Proxy en natif, utile pour de la validation automatique ou de l'observation :

function createValidatedStore<T extends object>(target: T): T {
  return new Proxy(target, {
    set(obj, prop, value) {
      // Validation : les strings ne peuvent pas être vides
      if (typeof value === 'string' && value.trim() === '') {
        throw new Error(`Property ${String(prop)} cannot be empty`);
      }
      obj[prop as keyof T] = value;
      return true;
    },
  });
}

const user = createValidatedStore({ name: 'Alice', email: 'alice@example.com' });
user.name = ''; // Error: Property name cannot be empty

Lazy Proxy

Pour des objets coûteux à initialiser :

function createLazy<T>(factory: () => T): T {
  let instance: T | null = null;

  return new Proxy({} as T, {
    get(_, prop) {
      if (!instance) instance = factory();
      return (instance as Record<string | symbol, unknown>)[prop];
    },
  });
}

// La connexion à la DB n'est initialisée qu'au premier accès
const db = createLazy(() => new DatabaseConnection());

// ... plus tard dans le code
db.query('SELECT 1'); // initialisation ici seulement

En React : React.lazy

React.lazy est une implémentation du pattern Proxy pour le lazy loading de composants :

// Le composant n'est chargé qu'au premier rendu
const HeavyChart = React.lazy(() => import('./HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Afficher le graphe</button>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

Proxy vs Decorator

Les deux patterns enveloppent un objet et implémentent la même interface. La distinction :

  • Decorator : ajoute des fonctionnalités, enrichit le comportement
  • Proxy : contrôle l'accès, peut bloquer ou différer les appels

En pratique la frontière est floue. Un proxy de cache "décore" aussi l'objet. Ce qui compte c'est l'intention : contrôle d'accès (Proxy) vs enrichissement (Decorator).