L’utile costruttore

PL\SQL è un linguaggio procedurale ma è possibile adottare uno stile di scrittura orientato alla programmazione ad oggetti.

In un package PL\SQL è frequente dover definire un modello dati in memoria a supporto degli algoritmi di elaborazione.
Per far ciò, io personalmente preferisco utilizzare TYPE RECORD definiti all’interno del package stesso invece di TYPE OBJECT, perché ciò rende il package auto consistente.

Dato un TYPE RECORD è possibile definire uno o più RECORD di questo tipo di dato. L’inizializzazione di queste variabili può essere effettuati in punti diversi del codice nei modi più diversi, ma non si ha alcuna garanzia che il record venga inizializzato sempre in modo completo e corretto.
Questo rischio aumenta quando è necessario estendere il TYPE RECORD aggiungendo un attributo per esempio.
Il rischio è che, le procedure che utilizzano tali RECORD, risentano della inizializzazione incoerente di questi e che ciò produca errori a run-time.

Il concetto di costruttore è una soluzione efficace a questo problema: quando si usa una struttura dati, prima di usarla, è necessario avere garanzia che essa sia stata correttamente inizializzata.

Nel nostro caso l’idea è di considerare un TYPE RECORD l’equivalente di una CLASS e definire una FUNCTION che svolga il ruolo di costruttore.
Il costruttore accetta in ingresso tutta una serie di parametri che consentono di inizializzare il RECORD e restituisce il RECORD stesso.
La regola non scritta, si parla appunto di stile di programmazione, è che prima di usare RECORD di questo tipo si invoca sempre il suo costruttore.

SUBTYPE tItemId IS VARCHAR2(50);
SUBTYPE tCurrency IS NUMBER;

TYPE tItem IS RECORD(
   id     tItemId
  ,descr  VARCHAR2(255)
  ,price  tCurrency
  ,qty    NUMBER
  ,total  tCurrency
);
FUNCTION itemNew RETURN tItem IS
  this tItem :=NULL;
BEGIN
  this.qty   := 0;
  this.total := 0;
  RETURN this;
END;

FUNCTION itemNew(
   id IN tItemId
  ,descr IN VARCHAR2
  ,price  tCurrency
  ,qty    NUMBER
) RETURN tItem IS
  this tItem :=itemNew();
BEGIN
  this.id := id;
  this.descr := descr;
  this.price := price;
  this.qty := qty;
  RETURN this;
END;
DECLARE
  item tItem := itemNew();
BEGIN
  item := itemNew(id=>'ID123',descr=>'HAMMER',price=>1.3,qty=>2);
END;

Questo approccio può sembrare costoso a prima vista ma in realtà è semplice e facilmente ripetibile.

Il suo utilizzo rende il codice facilmente comprensibile.
Inoltre nel caso in cui si debba estendere il il TYPE RECORD, alterando il costruttore, avremo garanzia che l’intervento venga effettuato in modo coerente in tutti i punti del codice in cui il costruttore è invocato.

Il consiglio è di predisporre sempre un costruttore base non accetta alcun parametro e che inizializza tutti gli attributi ad un valore di default: nel gergo dei programmatori C un costruttore VOID.

Naturalmente sfruttando l’OVERLOADING è possibile definire una famiglia di costruttori che espongono parametri diversi in base alle varie esigenze applicative.

Un cronografo piccolo piccolo

A volta capita che una procedura PL\SQL sia lenta.
In questi casi per individuare le porzioni di codice lento è utile misurarne il tempo di esecuzione.
Per far ciò ho realizzato un package che implementa un cronografo.

CREATE OR REPLACE PACKAGE CRONO IS
   TYPE tTimer IS RECORD(
      s TIMESTAMP
     ,e TIMESTAMP
   );
 FUNCTION  new     RETURN tTimer;
   PROCEDURE stop    (this IN OUT NOCOPY tTimer);
   PROCEDURE reset   (this IN OUT NOCOPY tTimer);
   FUNCTION  time    (this IN OUT NOCOPY tTimer) RETURN VARCHAR2;
   FUNCTION  hour    (this IN OUT NOCOPY tTimer) RETURN NUMBER;
   FUNCTION  minute  (this IN OUT NOCOPY tTimer) RETURN NUMBER;
   FUNCTION  second  (this IN OUT NOCOPY tTimer) RETURN NUMBER;
 END;
CREATE OR REPLACE PACKAGE BODY CRONO IS
  
  FUNCTION  new RETURN tTimer IS
    this tTimer := NULL;
  BEGIN
    this.s := SYSTIMESTAMP;
    this.e := NULL;
    RETURN this;
  END;
  
  PROCEDURE starting(this IN OUT NOCOPY tTimer)IS
  BEGIN
    this.s := SYSTIMESTAMP;
  END;
  
  PROCEDURE stop (this IN OUT NOCOPY tTimer) IS 
  BEGIN
    this.e := SYSTIMESTAMP;
  END;
  
  PROCEDURE reset (this IN OUT NOCOPY tTimer) IS
  BEGIN
    this.s := SYSTIMESTAMP;
    this.e := NULL;
  END;

  FUNCTION  time (this IN OUT NOCOPY tTimer) RETURN VARCHAR2 IS
    ret VARCHAR2(32767):=NULL;
  BEGIN
    ret := this.e - this.s;
    
    ret := ret || ' ' || 'h:' || (extract(hour from this.e - this.s));
    ret := ret || ' ' || 'm:' || (extract(minute from this.e - this.s));
    ret := ret || ' ' || 's:' || (extract(second from this.e - this.s));
    
    RETURN ret;
  END;

  FUNCTION  hour (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    h NUMBER :=NULL;
  BEGIN
    h := (extract(hour from this.e - this.s));
    RETURN h;
  END;

  FUNCTION  minute (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    m NUMBER :=NULL;
  BEGIN
    m := (extract(minute from this.e - this.s));
    RETURN m;
  END;

  FUNCTION  second (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    s NUMBER :=NULL;
  BEGIN
    s := (extract(second from this.e - this.s));
    RETURN s;
  END; 
END;

Il suo utilizzo è molto semplice.
A seguire un esempio:

DECLARE
  t1 CRONO.tTimer; --Variabile di tipo cronometro

  --Codice che consuma tempo
  PROCEDURE NOP(cycle IN NUMBER)IS
  BEGIN
    FOR i in 1..cycle LOOP
      DECLARE
       c NUMBER := 0;
      BEGIN
        select 1 into c from dual;
      END;
    END LOOP;
  END;

BEGIN
  --Il cronometro t1 viene creato ed avviato
  t1 := CRONO.new();
  
  NOP(cycle=>1000);
   
  --Il cronometro t1 viene fermato
  CRONO.stop(t1);

  --Si stampa il tempo misurato dal cronometro t1
  DBMS_OUTPUT.PUT_LINE('Tempo trascorso A:' || CRONO.time(t1));

  --Si resetta il cronometro t1
  CRONO.reset(t1); 
  
  NOP(cycle=>500);
 
  --Il cronometro t1 viene fermato
  CRONO.stop(t1);
   
  --Si stampa il tempo misurato dal cronometro t1
  DBMS_OUTPUT.PUT_LINE('Tempo trascorso B:' || CRONO.time(t1));
 
END;

“The database is just a detail…”

Questa frase può sembrare innocente ma no lo è.
Al contraio è dirompente.
Per chi lavora come me in PL\SQL il database è la piattaforma di lavoro quindi dire che il database è un dettaglio è sovversivo…a prima vista.
Già perchè se ci fermiamo a riflettere non saremo mai dei buoni specchi :).
In realtà il nostro software poggia saldamente sul DMBS ma non sul database, la differenza è significativa.
PL\SQL è la crasi di due elementi distinti PL e SQL.
SQL esiste solo in virtù del database (tabelle, viste ecc).
PL è un linguaggio procedurale che viene compilato ed eseguito da Oracle ma che funziona anche in assenza di oggetti di database.
E sai che scoperta!
Le implicazioni invece sono profonde.
Il codice PL puro, non facendo riferimento al database, può essere sottoposto a unit test automatici e ripetibili.
Fare la stessa cosa su codice PL\SQL (quello bello infarcito con select, insert ed update) è ben diverso perchè per effettuare un test è necessario creare il giusto contesto dati sul database, infatti un test dele genere è già system.
Se la tematica vi intriga ho approfondito il tutto in un ebook “PL/SQL OOP Style”.
La frase che da’ il titolo al post è una citazione del mitico Robert C. Martin dal libro “Clean Code” che continua ad essere per me un’ importante fonte di ispirazione, che bello sarebbe magari avere uno zio così.
La t-shirt è una mia creazione, una simpatica provocazione per i miei colleghi che hanno imparato a sopportarmi.