Eccoci di nuovo per proseguire nel percorso delle interfacce (Parte 3) (link al post precedente: https://blog.lazaruspascal.it/2023/09/25/interfacce-interface-parte-2/)
In questo post proveremo ad usare le interfacce.
Spero che tutti abbiamo chiaro i concetti dell’OOP (programmazione orientata agli oggetti), nel caso serva un rinfresco possiamo rinfrescare la mente leggendo questo articolo: http://www.lazaruspascal.it/index.php?page=53
Oltre a questi concetti, nella programmazione esistono anche altri paradigmi, correnti di pensiero più o meno assodate, regole e buona prassi, etc …
Alcuni di questi concetti si sono sufficientemente affermati e seguirli vuole dire produrre un software veramente di ottima qualità.
In questo link potete leggere un articolo su una serie di regole (in acronimo SOLID) che forniscono delle linee guida e dei suggerimenti sulla OOP (è in Italiano):
Consiglio vivamente a tutti (scafati o no) di leggere l’articolo: gli esempi non sono scritti in Pascal ma sono assolutamente comprensibili anche a chi non mastica alcun altro linguaggio.
Ho fatto questa premessa perchè le interfacce sono uno di quei strumenti (ma non sono i soli e da soli non sono sufficienti) che consentono di aderire a quei principi.
Conosciamo tutti il concetto di ereditarietà (inheritance), ossia che un oggetto può ereditare le caratteristiche (proprietà, funzioni, procedure, etc…) da un altro oggetto e “farle sue”.
Tutti i linguaggi di programmazione, almeno quelli che io conosco (ma magari li conosco male), non consentono l’ereditarietà multipla cioè ereditare da diversi oggetti che non siano degli “ancestor” dell’oggetto da cui stiamo derivando il modello.
Ossia per fare un esempio:
TQuadrato = class(TFigura)
La classe TQuadrato è derivata da TFigura e erediterà le sue caratteristiche (e quelle delle classi superiori).
Ma se io volessi che TQuadrato erediti si da TFigura ma anche da TPoligono, che non è un “ancestor” di TFigura, non sarebbe possibile. Posso estendere la classe con TPoligono ma non ha lo stesso significato ne la stessa funzionalità di una ereditarietà.
Ora, invece con ns. sorpresa (si, anche la mia la prima volta che l’ho visto fare), le interfacce ci vengono in aiuto e come vedremo ci consentono di ereditare (non è il termine corretto ma rende comunque l’idea) da più oggetti.
L’esempio che riporto è semplice e basilare ma vuole solo fare capire che l’uso delle interfacce consente di astrarre il codice mantenendo comunque alcune regole del buon programmatore.
L’esempio può essere anche svolto usando solo classi, ma ciò andrebbe probabilmente (anzi sicuramente) contro i concetti SOLID.
L’esempio è uno spunto tratto da una pubblicazione di Nick Hodges (More Coding in Delphi) che lavora presso Embarcadero.
Incominciamo a definire alcune interfacce che sfrutteremo poi nelle classi. I GUID (per esempio {CEE797D9-C71D-419F-ACB8-2F17B3D42382}) sono gli indentificativi delle interfacce e sono usati quando si ha a che fare con ActiveX e con COM oppure quando si devono usare le RTTI come “as” e “is”. Ogni interfaccia deve avere un GUID UNICO !!!
Se non avete tali necessità, le GUID non sono necessarie (si possono omettere).
Queste interfacce derivano da IInterface (che ricordo è l’interfaccia base sia in Delphi che in FPC) e definiscono delle procedure, e come abbiamo già riportato nei precedenti articoli queste procedure non sono implementate.
unit uMovimenti;
interface
type
ICorre = interface(IInterface)
['{CEE797D9-C71D-419F-ACB8-2F17B3D42382}']
procedure Corre;
end;
INuota = interface(IInterface)
['{E00BD402-36FE-4928-9940-C6EF69A191EC}']
procedure Nuota;
end;
//Questa non la useremo
ISalta = interface(IInterface)
['{3B8E4264-2D67-42D4-8FCD-FCABE4DF6DB1}']
procedure Salta;
end;
IVola = interface(IInterface)
['{B2FCB64B-C3A9-4212-AD43-2C02E7F54408}']
procedure Vola;
end;
implementation
end.
Ora definiamo le classi che implementeranno tali interfacce.
unit uAnimaliClasses;
{$mode ObjFPC}{$H+}
interface
uses
uMovimenti, uLogEvent;
type
{$REGION 'Documentation'}
/// <summary>
/// Classe base per tutti gli animali.
/// </summary>
/// <remarks>
/// <para>
/// Implementa <see cref="System|IInterface" /> come discendente e può
/// implementare le interfacce da <see cref="uMovimenti" />.
/// </para>
/// <para>
/// Discende da <see cref="System|TInterfacedObject" /> per
/// la <see cref="System|IInterface" /> per funzionare.
/// </para>
/// <para>
/// Implementa <see cref="OnLog" /> per poter effettuare un Log.
/// </para>
/// </remarks>
{$ENDREGION}
TAnimali = class(TInterfacedObject, IInterface)
strict private
FOnLog: TLogEvent;
strict protected
procedure Log(const Line: string); virtual;
procedure LogMethod(const MethodNam: string); virtual;
public
constructor Create(const LogEvent: TLogEvent = nil); virtual;
property OnLog: TLogEvent read FOnLog write FOnLog;
end;
{$REGION 'Documentation'}
/// <summary>
/// Per la creazione delle classi derivate da <see cref="TAnimal" />.
/// Richiede un virtual <see cref="TAnimal.Create">Create</see> constructor.
/// </summary>
{$ENDREGION}
TAnimaliClass = class of TAnimali;
{$REGION 'Documentation'}
/// <remarks>
/// Non tutti gli uccelli volano, e non solo gli uccelli volano,
/// così creiamo delle sottoclassi comuni vuote o no
/// <see cref="uMovimenti|IVola" />
/// </remarks>
{$ENDREGION}
TUccelli = class(TAnimali)
end;
TAnatra = class(TUccelli, IVola, INuota, ICorre)
public
procedure Vola;
procedure Corre;
procedure Nuota;
end;
TStruzzo = class(TUccelli, ICorre)
public
procedure Corre;
end;
TPinguino = class(TUccelli, INuota, ICorre)
public
procedure Nuota;
procedure Corre;
end;
//Pesci, generalmente nuotano tutti quindi già deriviamo
//l'interfaccia del nuoto
TPesci = class(TAnimali, INuota)
public
procedure Nuota;
end;
TArringa = class(TPesci)
end;
TSalmone = class(TPesci)
end;
//Test per l'override di Nuota dichiarato sia in TPesci che in TSqualo
TSqualo = class(TPesci, INuota)
procedure Nuota;
end;
//Insetti, generalmente corrono tutti quindi deriviamo
//l'interfaccia ICorre
TInsetti = class(TAnimali, ICorre)
public
procedure Corre;
end;
TFormica = class(TInsetti)
end;
TFarfalla = class(TInsetti, IVola)
public
procedure Vola;
end;
//I mammiferi non hanno caratteristiche comuni,
//qualcuno vola, qualcuno nuota, altri corrono
//quindi la classe base è vuota
TMammiferi = class(TAnimali)
end;
TPippistrello = class(TMammiferi, IVola)
public
procedure Vola;
end;
TGatto = class(TMammiferi, ICorre)
public
procedure Corre;
end;
TCane = class(TMammiferi, INuota, ICorre)
public
procedure Corre;
procedure Nuota;
end;
TBalena = class(TMammiferi, INuota)
public
procedure Nuota;
end;
implementation
uses
SysUtils;
constructor TAnimali.Create(const LogEvent: TLogEvent = nil);
begin
inherited Create();
OnLog := LogEvent;
end;
procedure TAnimali.Log(const Line: string);
var
LogEvent: TLogEvent;
begin
LogEvent := OnLog; // Occhio che non siamo THREAD SAFE
if Assigned(LogEvent) then
LogEvent(Line);
end;
procedure TAnimali.LogMethod(const MethodNam: string);
var
Line: string;
begin
Line := Format('%s.%s', [ClassName, MethodNam]);
Log(Line);
end;
procedure TAnatra.Vola;
begin
LogMethod('Vola');
end;
procedure TAnatra.Corre;
begin
LogMethod('Corre come una papera ...');
end;
procedure TAnatra.Nuota;
begin
LogMethod('Nuota');
end;
procedure TStruzzo.Corre();
begin
LogMethod('Corre');
end;
procedure TPinguino.Nuota();
begin
LogMethod('Nuota');
end;
procedure TPinguino.Corre();
begin
LogMethod('Corre');
end;
procedure TPesci.Nuota();
begin
LogMethod('Nuota');
end;
procedure TSqualo.Nuota();
begin
LogMethod('Nuota e .... mangia');
end;
procedure TInsetti.Corre();
begin
LogMethod('Corre');
end;
procedure TFarfalla.Vola();
begin
LogMethod('Vola quando non è una crisalide');
end;
procedure TPippistrello.Vola();
begin
LogMethod('Vola');
end;
procedure TGatto.Corre();
begin
LogMethod('Corre');
end;
procedure TCane.Corre();
begin
LogMethod('Corre');
end;
procedure TCane.Nuota();
begin
LogMethod('Nuota');
end;
procedure TBalena.Nuota();
begin
LogMethod('Nuota');
end;
end.
L’unita di uLogEvent definisce un modo comune per visualizzare delle stringhe come un “log” dentro un memo.
unit uLogEvent;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils;
type
TLogEvent = procedure(const Line: string) of object;
//TLogEvent = reference to procedure(const Line: string);
implementation
end.
Ciò che invece volevo evidenziare, prendendo ad esempio TAnatra e TFarfalla e TSalmone e TSqualo è l’uso delle interfacce:
Come vedete, nella definizione di TAnatra abbiamo inserito tre interfacce derivando da TUccelli (dove non abbiamo inserito interfacce) e quindi nella implementazione TAnatra implementa tre metodi (Corre, Vola, Nuota).
In TFarfalla, invece abbiamo inserito solo una interfaccia (IVola) con Vola che viene implementato ma derivando da TInsetti che già definisce “CORRE” si trova quindi già definito anche il metodo Corre, per ereditarietà e quindi potranno essere usate sia la procedura Vola che Corre.
In TSalmone ad esempio, non abbiamo inserito interfacce in quanto la classe “ancestor” TPesci ha gia derivato la interfaccia INuota (con la procedura Nuota()) che quindi è già a disposizione di tutto le sottoclassi.
In TSqualo abbiamo inserito l’interfaccia (INuota), nonostante la classe “ancestor” TPesci già lo includa. In questo caso viene effettuato l’override dell’interfaccia e la procedura NUOTA che verrà eseguita sarà quello di TSqualo e non quella di TPesci.
Si poteva fare anche con le classi ? Si, probabilmente si ma ci saremmo sicuramente caricati di procedure che non potevano essere usate, con un dispendio di IF, CASE, etc… Inoltre se aggiungiamo ad esempio il “Salta”
le due classi basi non cambiano e possiamo inserire l’apposita interfaccia solo “all’animale” che può effettivamente saltare.
Altra cosa da notare: a parte il LOG… non ci sono variabili dichiarate, ne classi assegnate … (tipo var Squalo: TSqualo) … e vedremo che neanche ci saranno come si potrebbe pensare… questo è il bello.
Ciao, alla prossima (qui il link: https://blog.lazaruspascal.it/2023/09/25/interfacce-interface-parte-4/)