Conceptual vorbind, termenul de proxy are aceeași conotație atunci când vorbim de sisteme software: un intermediar între 2 părți.
Fie că vorbim de 2 sisteme ce comunică între ele, fie că vorbim la nivel de componente de cod, termenul de “proxy” ne duce cu gândul la același concept.
Am mai vorbit în trecut pe tema unui proxy în contextul comunicării între servicii, nu între 2 componente de cod, aici.
La nivelul componentelor de cod, proxy design pattern sau “șablon” proxy, dacă vrei, joacă un rol foarte important și ne ajută să rezolvăm una din cele mai critice probleme de inginerie software.
Problema
În dezvoltarea sistemelor software, execuția codului nostru nu are, în general, nevoie de atât de multă putere de procesare pe cât am crede.
Limbajele și platformele de dezvoltare au devenit în prezent extrem de eficiente din punctul ăsta de vedere și execuția codului a devenit destul de lejeră.
În schimb, există componente care sunt extrem de mari consumatoare de resurse și cu care comunicarea devine lentă destul de repede, în momentul în care numărul de apeluri către aceste resurse crește vertiginos.

Una dintre aceste resurse este baza de date, care este și una din resursele cele mai importante din componența unui sistem software.
Trimiterea unui număr mare de apeluri, în paralel, către o bază de date, ne va încetini drastic timpul de execuție al aplicației noastre.
Ok și ce putem face în acest caz? Sistemul nostru presupune un număr mare de apeluri către baza de date, pentru că extragem cantități mari de date de acolo.
Soluția - implementarea unui proxy pentru cache
Soluția salvatoare e adăugarea unui serviciu de cache în aplicația noastră, care să stocheze temporar datele cele mai frecvent utilizate.
Acest serviciu de cache va juca rol de intermediar cu orice client care vrea să apeleze baza de date.

Astfel, serviciile mari consumatoare de resurse, cum este în acest caz baza de date, sunt protejate și au parte de un număr de apeluri mult mai redus și drept urmare performează mult mai bine.

Implementarea unui proxy
Pentru implementare, vom crea un serviciu de cache care va intercepta toate apelurile dintre clienți și baza de date și va salva datele în cache pe măsură ce se fac citiri din baza de date.

Ambele servicii, atât cel de cache, cât și cel al bazei de date își vor ascunde implementările în spatele aceleiași interfețe, astfel încât orice client să poată opera cu date atât din cache, cât și din baza de date direct.
Definim interfața comună, care va fi folosită de ambele servicii.
interface IProductService
{
List<Product> GetAll();
Product? GetById(int id);
void Upsert(Product product);
void Delete(Product product);
}
Vom simula desigur comunicarea cu o bază de date, dar poți considera că, cel puțin în .NET, un serviciu de acces al bazei de date va arăta extrem de similar.
class DatabaseService : IProductService
{
private readonly List<Product> _db = [];
public List<Product> GetAll()
{
return _db;
}
public Product? GetById(int id)
{
return _db.FirstOrDefault(x => x.Id == id);
}
public void Delete(Product product)
{
_db.Remove(product);
}
public void Upsert(Product product)
{
var existingProduct = _db.FirstOrDefault(x => x.Id == product.Id);
if (existingProduct != null)
{
existingProduct.Description = product.Description;
}
else
{
_db.Add(product);
}
}
}
În cazul nostru serviciul proxy va fi CacheService
, iar dacă datele necesare nu sunt prezente în cache va trimite apelul către baza de date.
class CacheService(IProductService database) : IProductService
{
private readonly IProductService _database = database;
private readonly List<Product> _cache = [];
public Product? GetById(int id)
{
Product? product = _cache.FirstOrDefault();
if (product != null)
{
return product;
}
else
{
product = _database.GetById(id);
if (product != null)
{
// salvăm datele în cache înainte să le întoarcem la client
_cache.Add(product);
}
return product;
}
}
public List<Product> GetAll()
{
var products = _database.GetAll();
// salvăm datele în cache înainte să le întoarcem la client
_cache.AddRange(products);
return products;
}
public void Upsert(Product product)
{
_database.Upsert(product);
// invalidăm cache-ul pentru produsul respectiv
_cache.Remove(product);
}
public void Delete(Product product)
{
_database.Delete(product);
// invalidăm cache-ul pentru produsul respectiv
_cache.Remove(product);
}
}
Iar codul client va folosi întotdeauna IProductService pentru a comunica cu baza de date.
class Client(IProductService productService)
{
private readonly IProductService _productService = productService; // serviciu proxy
public void PrintProducts()
{
var products = _productService.GetAll();
foreach (var product in products)
{
Console.WriteLine(product.Description);
}
}
}
Evident, lucrurile au fost cu mult simplificate, dar ideea de bază rămâne aceeași. Clienții folosesc un serviciu intermediar pentru a citi datele, iar serviciul va delega apelurile către un layer de cache și către baza de date, atunci când datele sunt absente în cache.
În general datele din cache au și un timp de expirare, dar am omis asta, pentru că scopul articolului nu era să implementăm un cache, ci să înțelegem cum se folosește proxy design pattern și ce problemă rezolvă.
Concluzie
Deși exemplul folosit e extrem de simplificat, este totuși destul de apropiat de ceea ce o să găsești într-o aplicație reală de producție.
Exemplul cu cache e unul din cele mai intâlnite implementări pentru un proxy, dar el există peste tot.
Gândește-te că orice platformă de servicii este de fapt un proxy:
- Platforme pentru rezervări de cazare, precum Booking sau Airbnb - proxy pentru cei ce au unități de cazare de închiriat
- Brokeri de asigurare - proxy pentru asiguratori
- Un translator - proxy pentru comnicarea între 2 părți
Și exemplele pot continua, dar cel mai ușor e să identifici intermediarii în anumite procese din viața reală. Acei intermediari pot fi asociați conceptului de proxy din software.
Cam atât pentru azi, pe data viitoare.