Le collection generiche di System.Generics.Collections sono uno degli strumenti più sottoutilizzati di Delphi: permettono di scrivere codice riutilizzabile senza rinunciare al type-checking a compile-time. Vediamo due pattern pratici — un dizionario chiave-valore incapsulato in una classe di servizio, e un filtro generico per array — che tornano utili in tanti progetti reali.
TDictionary<K,V>: incapsulare o ereditare?
Il modo più semplice per lavorare con un TDictionary è nasconderlo dietro una classe che ne gestisce il ciclo di vita ed espone solo i metodi che servono davvero:
unit ServizioAnagrafica;
interface
uses
System.Generics.Collections, System.SysUtils, System.Classes;
type
TAnagraficaCache = class
private
FDati: TDictionary<string, string>;
public
constructor Create;
destructor Destroy; override;
procedure Imposta(const Chiave, Valore: string);
function Ottieni(const Chiave: string; out Valore: string): Boolean;
procedure Rimuovi(const Chiave: string);
function TutteLeVoci: TStringList;
end;
implementation
constructor TAnagraficaCache.Create;
begin
FDati := TDictionary<string, string>.Create;
end;
destructor TAnagraficaCache.Destroy;
begin
FDati.Free;
inherited;
end;
procedure TAnagraficaCache.Imposta(const Chiave, Valore: string);
begin
// AddOrSetValue evita l'eccezione se la chiave esiste già
FDati.AddOrSetValue(Chiave, Valore);
end;
function TAnagraficaCache.Ottieni(const Chiave: string; out Valore: string): Boolean;
begin
Result := FDati.TryGetValue(Chiave, Valore);
end;
procedure TAnagraficaCache.Rimuovi(const Chiave: string);
begin
FDati.Remove(Chiave);
end;
function TAnagraficaCache.TutteLeVoci: TStringList;
var
Chiave: string;
begin
Result := TStringList.Create;
for Chiave in FDati.Keys do
Result.Add(Format('%s: %s', [Chiave, FDati[Chiave]]));
end;
end.
Questo approccio "a incapsulamento" ha un vantaggio: la classe che usa TAnagraficaCache non sa nemmeno che sotto c'è un TDictionary, quindi in futuro potresti sostituirlo con un'altra struttura dati senza toccare il resto del codice.
L'alternativa è ereditare direttamente da TDictionary<K,V> per aggiungere metodi specifici, quando ti interessa comunque esporre l'intera interfaccia della collezione:
type
TDizionarioContatori = class(TDictionary<Integer, string>)
public
procedure ImpostaOAggiorna(Chiave: Integer; const Valore: string);
function RiepilogoComeTesto: string;
end;
procedure TDizionarioContatori.ImpostaOAggiorna(Chiave: Integer; const Valore: string);
begin
AddOrSetValue(Chiave, Valore);
end;
function TDizionarioContatori.RiepilogoComeTesto: string;
var
Chiave: Integer;
begin
Result := '';
for Chiave in Keys do
Result := Result + Format('%d: %s'#13#10, [Chiave, Items[Chiave]]);
end;
In generale conviene incapsulare quando la collezione è un dettaglio implementativo, ed ereditare solo quando vuoi davvero che il chiamante tratti l'oggetto come "un dizionario con qualcosa in più".
Filtrare un array con una classe generica
Lo stesso principio si applica bene anche a TArray<T>. Una classe generica di filtro puoi scriverla una volta e riutilizzarla per qualsiasi tipo, incluso un record:
type
TFiltroArray<T> = class
private
FSorgente: TArray<T>;
public
constructor Create(const ASorgente: TArray<T>);
function Filtra(const Predicato: TFunc<T, Boolean>): TArray<T>;
end;
constructor TFiltroArray<T>.Create(const ASorgente: TArray<T>);
begin
FSorgente := ASorgente;
end;
function TFiltroArray<T>.Filtra(const Predicato: TFunc<T, Boolean>): TArray<T>;
var
Elemento: T;
begin
SetLength(Result, 0);
for Elemento in FSorgente do
if Predicato(Elemento) then
begin
SetLength(Result, Length(Result) + 1);
Result[High(Result)] := Elemento;
end;
end;
Passare un TFunc<T, Boolean> come predicato, invece di un valore fisso da cercare come nell'esempio originale da cui questa classe è ispirata, rende il filtro molto più flessibile: puoi confrontare un campo Nome, un intervallo di date o qualunque condizione, senza scrivere una nuova classe ogni volta.
var
Prodotti, Disponibili: TArray<TProdottoRecord>;
Filtro: TFiltroArray<TProdottoRecord>;
begin
Filtro := TFiltroArray<TProdottoRecord>.Create(Prodotti);
try
Disponibili := Filtro.Filtra(
function(const P: TProdottoRecord): Boolean
begin
Result := P.Disponibile;
end);
finally
Filtro.Free;
end;
end;
Con poche righe ottieni una piccola libreria interna di utility generiche che puoi condividere fra i progetti, senza duplicare la stessa logica di ricerca in ogni form.