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 — это контролируемый доступ к объектам!