Lapa Knowledge Base
Структурные паттерны

Proxy

Паттерн Proxy

Proxy — это структурный паттерн проектирования, который предоставляет объект-заместитель или заполнитель для другого объекта, чтобы контролировать доступ к нему.

Проблема

Зачем контролировать доступ к объекту? Представьте, что у вас есть объект, который выполняет тяжелые операции. Вы можете отложить инициализацию такого объекта до момента, когда он действительно понадобится, и повторно использовать уже созданный экземпляр.

Но как реализовать это прозрачно? Клиентский код не должен знать, работает ли он с реальным объектом или с его заместителем.

Решение

Паттерн Proxy решает эту проблему, создавая объект-заместитель с тем же интерфейсом, что и оригинальный объект. Когда клиент запрашивает операцию, заместитель может выполнить её сам или передать запрос оригинальному объекту.


Структура

Subject
└── request(): void

RealSubject
└── request(): void

Proxy
├── realSubject: RealSubject
├── request(): void
└── lazyInitialization(): void

Client
└── clientCode(subject: Subject): void

Пример реализации

JavaScript

// Интерфейс субъекта
class DatabaseInterface {
    query(sql) {
        throw new Error('Метод query должен быть переопределен');
    }
    
    connect() {
        throw new Error('Метод connect должен быть переопределен');
    }
    
    disconnect() {
        throw new Error('Метод disconnect должен быть переопределен');
    }
}

// Реальный субъект
class RealDatabase extends DatabaseInterface {
    constructor(connectionString) {
        super();
        this.connectionString = connectionString;
        this.isConnected = false;
    }
    
    connect() {
        console.log(`Подключение к базе данных: ${this.connectionString}`);
        // Имитация времени подключения
        this.isConnected = true;
    }
    
    query(sql) {
        if (!this.isConnected) {
            throw new Error('База данных не подключена');
        }
        
        console.log(`Выполнение запроса: ${sql}`);
        // Имитация выполнения запроса
        return {
            rows: [
                { id: 1, name: 'John' },
                { id: 2, name: 'Jane' }
            ],
            affected: 2
        };
    }
    
    disconnect() {
        console.log('Отключение от базы данных');
        this.isConnected = false;
    }
}

// Заместитель с ленивой инициализацией
class DatabaseProxy extends DatabaseInterface {
    constructor(connectionString) {
        super();
        this.connectionString = connectionString;
        this.realDatabase = null;
    }
    
    connect() {
        if (!this.realDatabase) {
            console.log('Создание подключения к базе данных...');
            this.realDatabase = new RealDatabase(this.connectionString);
        }
        this.realDatabase.connect();
    }
    
    query(sql) {
        if (!this.realDatabase) {
            console.log('Автоматическое подключение к базе данных...');
            this.connect();
        }
        
        return this.realDatabase.query(sql);
    }
    
    disconnect() {
        if (this.realDatabase) {
            this.realDatabase.disconnect();
        }
    }
}

// Использование
function testDatabase(database) {
    console.log('=== Тестирование базы данных ===');
    
    try {
        // Запрос без предварительного подключения
        const result = database.query('SELECT * FROM users');
        console.log('Результат запроса:', result);
        
        // Еще один запрос
        const result2 = database.query('SELECT COUNT(*) FROM users');
        console.log('Результат второго запроса:', result2);
        
    } catch (error) {
        console.error('Ошибка:', error.message);
    } finally {
        database.disconnect();
    }
}

// Тестируем с реальной базой данных
console.log('--- Реальная база данных ---');
const realDB = new RealDatabase('mysql://localhost:3306/mydb');
testDatabase(realDB);

console.log('\n--- Заместитель базы данных ---');
const proxyDB = new DatabaseProxy('mysql://localhost:3306/mydb');
testDatabase(proxyDB);

TypeScript с интерфейсами

// Интерфейс изображения
interface Image {
    display(): void;
}

// Реальное изображение
class RealImage implements Image {
    private filename: string;
    
    constructor(filename: string) {
        this.filename = filename;
        this.loadFromDisk();
    }
    
    private loadFromDisk(): void {
        console.log(`Загрузка изображения с диска: ${this.filename}`);
        // Имитация времени загрузки
    }
    
    display(): void {
        console.log(`Отображение изображения: ${this.filename}`);
    }
}

// Заместитель изображения
class ProxyImage implements Image {
    private realImage: RealImage | null = null;
    private filename: string;
    
    constructor(filename: string) {
        this.filename = filename;
    }
    
    display(): void {
        if (!this.realImage) {
            console.log('Создание реального изображения...');
            this.realImage = new RealImage(this.filename);
        }
        this.realImage.display();
    }
}

// Виртуальный заместитель с кэшированием
class VirtualProxyImage implements Image {
    private realImage: RealImage | null = null;
    private filename: string;
    private isLoaded: boolean = false;
    
    constructor(filename: string) {
        this.filename = filename;
    }
    
    display(): void {
        if (!this.isLoaded) {
            console.log(`Загрузка изображения: ${this.filename}`);
            this.realImage = new RealImage(this.filename);
            this.isLoaded = true;
        }
        this.realImage!.display();
    }
    
    // Метод для предварительной загрузки
    preload(): void {
        if (!this.isLoaded) {
            console.log(`Предварительная загрузка: ${this.filename}`);
            this.realImage = new RealImage(this.filename);
            this.isLoaded = true;
        }
    }
}

// Использование
function testImageDisplay(image: Image): void {
    console.log('=== Отображение изображения ===');
    image.display();
    console.log('--- Повторное отображение ---');
    image.display();
}

// Тестируем разные типы заместителей
console.log('--- Обычный заместитель ---');
const proxyImage = new ProxyImage('photo.jpg');
testImageDisplay(proxyImage);

console.log('\n--- Виртуальный заместитель ---');
const virtualImage = new VirtualProxyImage('large-photo.jpg');
testImageDisplay(virtualImage);

console.log('\n--- Предварительная загрузка ---');
const preloadedImage = new VirtualProxyImage('huge-photo.jpg');
preloadedImage.preload();
testImageDisplay(preloadedImage);

Когда использовать

Используйте Proxy, когда:

  • Нужен более универсальный или сложный указатель на объект, чем простой указатель
  • Нужно контролировать доступ к объекту
  • Нужна ленивая инициализация дорогих объектов
  • Нужно кэширование результатов запросов
  • Нужно логирование доступа к объекту

Преимущества

  • Контролирует доступ к реальному объекту
  • Может работать с объектом, даже если он не создан
  • Может управлять жизненным циклом объекта
  • Принцип открытости/закрытости: можно добавлять новые заместители

Недостатки

  • Усложняет код из-за введения дополнительных классов
  • Может снизить производительность из-за дополнительного уровня абстракции
  • Может скрыть проблемы с реальным объектом

Отличия от других паттернов

  • Adapter — изменяет интерфейс объекта
  • Decorator — добавляет функциональность объекту
  • Facade — предоставляет упрощенный интерфейс к подсистеме

Реальный пример

// Заместитель для HTTP API
class ApiProxy {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.cache = new Map();
        this.requestQueue = new Map();
    }
    
    async get(endpoint) {
        const url = `${this.baseUrl}${endpoint}`;
        
        // Проверяем кэш
        if (this.cache.has(url)) {
            console.log(`Получение из кэша: ${url}`);
            return this.cache.get(url);
        }
        
        // Проверяем, не выполняется ли уже запрос
        if (this.requestQueue.has(url)) {
            console.log(`Ожидание выполнения запроса: ${url}`);
            return this.requestQueue.get(url);
        }
        
        // Создаем новый запрос
        console.log(`Выполнение HTTP запроса: ${url}`);
        const requestPromise = this.performRequest(url);
        
        // Добавляем в очередь
        this.requestQueue.set(url, requestPromise);
        
        try {
            const result = await requestPromise;
            
            // Кэшируем результат
            this.cache.set(url, result);
            
            // Удаляем из очереди
            this.requestQueue.delete(url);
            
            return result;
        } catch (error) {
            // Удаляем из очереди при ошибке
            this.requestQueue.delete(url);
            throw error;
        }
    }
    
    async performRequest(url) {
        // Имитация HTTP запроса
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        return {
            url: url,
            data: { message: 'Данные с сервера' },
            timestamp: new Date().toISOString()
        };
    }
    
    clearCache() {
        this.cache.clear();
        console.log('Кэш очищен');
    }
    
    getCacheSize() {
        return this.cache.size;
    }
}

// Использование
const api = new ApiProxy('https://api.example.com');

async function testApi() {
    console.log('=== Тестирование API заместителя ===');
    
    try {
        // Первый запрос (будет выполнен)
        const result1 = await api.get('/users');
        console.log('Результат 1:', result1);
        
        // Второй запрос (будет взят из кэша)
        const result2 = await api.get('/users');
        console.log('Результат 2:', result2);
        
        // Третий запрос (новый endpoint)
        const result3 = await api.get('/posts');
        console.log('Результат 3:', result3);
        
        console.log(`Размер кэша: ${api.getCacheSize()}`);
        
    } catch (error) {
        console.error('Ошибка:', error.message);
    }
}

testApi();

Proxy — это контролируемый доступ к объектам!

Copyright © 2026