#3 – Interfacce | Un esempio pratico

Nel post precedente abbiamo visto la definizione di interfacce e i vantaggi a esse collegati, sia in generale, sia declinate al mondo LabVIEW. Questo post riprende quindi i concetti visti illustrando un esempio pratico sull’uso di interfacce e classi. L’obiettivo del post è quello di fornire maggiori spunti e strumenti per utilizzare le interfacce con problemi reali.

Requisiti

Supponiamo di dover sviluppare un’applicazione per la gestione di una stazione di monitoraggio della qualità dell’aria. I requisiti prevedono che:

  • L’applicazione gestisca una stazione di monitoraggio della qualità dell’aria (stazione principale), alla quale sono connessi dei sensori e un’altra stazione (stazione periferica).
  • L’applicazione legga con un rate prefissato i valori di PM10 e di concentrazione di monossido di carbonio CO.
  • L’applicazione legga da due sensori di polveri sottili PM10 e di monossido di carbonio CO, connessi alla stazione di monitoraggio principale.
  • L’applicazione legga i valori di PM10 e CO acquisiti dalla stazione di monitoraggio periferica.
  • L’applicazione logghi tutti i dati acquisiti su un terminale console.
  • L’applicazione debba essere aperta ad estensioni come il supporto ad altre tipologie di sensori e ad altre modalità di logging.
Progettazione

Ora che abbiamo i requisiti dell’applicativo, passiamo alla progettazione di classi e interfacce.

Innanzitutto, prevediamo la classe Measurement usata per contenere tutti i dati relativi ad una misurazione: valore, unità di misura e la sorgente del dato di misura.

Classe Measurement.

Passiamo ora alla parte di logging.
I requisiti richiedono di loggare i dati acquisiti su un terminale console. Creiamo quindi l’interfaccia DataLogger che definisce il metodo LogData e la classe ConsoleLogger che implementa tale interfaccia. L’uso dell’interfaccia DataLogger rende l’applicazione aperta ad altre modalità di logging. A titolo di esempio, definiamo anche le classi SQLDatabase e FileLogger. Entrambe implementano l’interfaccia DataLogger.
Prevediamo poi la classe DataLoggerManager che ha la responsabilità di gestire il logging dei dati. La classe DataLoggerManager possiede un attributo di tipo DataLogger (relazione di composizione). In questo modo, poiché la classe DataLoggerManager dipende dall’interfaccia DataLogger e non da una classe concreta, è possibile cambiare le modalità di logging iniettando un qualsiasi oggetto che soddisfi l’interfaccia DataLogger.
Di seguito è riportato il diagramma UML dell’interfaccia DataLogger, le classi che la implementano e la classe DataLoggerManager.

Diagramma UML di classi e interfacce relative alla gestione del logging.

Procediamo la progettazione di classi e interfacce definendo l’architettura relativa ai sensori. Utilizziamo la classe base Sensor che definisce attributi come Name (nome del sensore), Serial Number, LastCalibration (data di ultima calibrazione) e metodi come Calibrate. Si noti che, ad eccezione di Name, i membri della classe Sensor non servono a soddisfare i requisiti riportati sopra. Servono invece a rendere l’esempio più vicino ad un’applicazione reale.
Rappresentiamo poi i sensori di polveri sottili PM10 e monossido di carbonio CO tramite due classi concrete derivate da Sensor. Le due classi sono rispettivamente la classe PM10Sensor e COSensor.
L’abilità dei due sensori di acquisire il valore di concentrazione di PM10 e di CO nell’aria non è definita nella classe Sensor. Sono invece definite due interfacce dedicate:

  • CanMeasurePM10 che definisce il metodo GetPM10Value.
  • CanMeasureCO che definisce il metodo GetCOValue.

Utilizzando le due interfacce sopra, le due classi che modellizzano i due tipi di sensori implementeranno solo l’interfaccia che effettivamente soddisfano. La classe PM10Sensor infatti implementa l’interfaccia CanMeasurePM10 e la classe COSensor implementa l’interfaccia CanMeasureCO.
Con questo approccio, se dovessimo aggiungere un nuovo tipo di sensore in grado di acquisire sia la concentrazione di PM10 che quella di CO, creeremo una nuova classe che implementerà entrambe le interfacce. Inoltre, se in futuro dovessimo acquisire un nuovo tipo di dato, creeremo una nuova interfaccia dedicata, senza toccare il codice esistente. Ciò soddisfa uno dei requisiti specificati sopra.
Di seguito è riportato il diagramma UML delle classi che modellizzano i sensori e le interfacce ad esse associate.

Diagramma UML di classi e interfacce relative alla gestione dei sensori.

Concludiamo la progettazione di classi e interfacce modellizzando le stazioni di monitoraggio. Anche in questo caso prevediamo una classe base (AirMonitoringStation) e due classi derivate: MainStation e PeripheralStation.
La classe AirMonitoringStation definisce le proprietà Name (che è un nome rappresentativo per la stazione) e Location (che indica la posizione della stazione di monitoraggio). Anche in questo caso, si noti che la proprietà Location non è necessaria per il soddisfacimento dei requisiti ma rende più concreto l’esempio corrente.
Come da requisiti, la stazione periferica è in grado di fornire alla stazione principale i valori di concentrazione di PM10 e di CO. Dal punto di vista dell’acquisizione dati, quindi, la stazione periferica ha lo stesso ruolo dei due sensori di PM10 e di CO. Per questa ragione e per semplificare il lavoro alla stazione principale, sfruttiamo le stesse interfacce usate per i due sensori. Poiché la stazione periferica fornisce sia il valore di concentrazione di PM10 che quello di CO, la classe PeripheralStation implementa entrambe le interfacce: CanMeasurePM10 e CanMeasureCO.
La classe MainStation ha il compito di prelevare i valori di PM10 e CO dalle sorgenti dato che gestisce (cioè dai sensori e dalla stazione periferica). Grazie all’uso delle interfacce, la classe MainStation non dipende dalle classi concrete ma dalle interfacce stesse, disaccoppiando quindi tra loro le classi concrete e rendendo l’applicativo più flessibile, più facilmente manutenibile e testabile.
La classe MainStation, quindi, ha nei proprio attributi due liste, rispettivamente di tipo CanMeasurePM10 e CanMeasureCO. In aggiunta, la classe ha due metodi dedicati per leggere i valori di PM10 e di CO da tutte le sorgenti dato che gestisce. Tali metodi sono: GetPM10Values e GetCOValues.
Di seguito è riportato il diagramma UML delle classi che modellizzano le stazioni di monitoraggio e delle interfacce coinvolte.

Diagramma UML di classi e interfacce relative alla gestione delle stazioni di monitoraggio.

Il diagramma UML di seguito riporta infine tutte le classi e interfacce definite e le loro relazioni. Per una più facile lettura del diagramma, la classe Measurement non è riportata.

Diagramma UML di classi e interfacce di tutta l’applicazione.
Inplementazione

Implementiamo passo passo le classi e interfacce progettate sopra. In particolare, riporto frammenti di codice e screenshot sia per la versione LabVIEW che per la versione C#.

Partiamo dalla classe Measurement:

public class Measurement
{
    public double Value { get; set; }
    public string Unit { get; set; }
    public string Source { get; set; }

    public Measurement(double value, string unit, string source)
    {
        this.Value = value;
        this.Unit = unit;
        this.Source = source;
    }
}
LabVIEW | Classe Measurement.

Passiamo poi alle classi relative alla gestione dei sensori (classi Sensor, PM10Sensor e COSensor) e alle interfacce ad esse associate (CanMeasurePM10 e CanMeasureCO).
Le due interfacce CanMeasurePM10 e CanMeasureCO definiscono rispettivamente i metodi GetPM10Value e GetCOValue. Entrambi i metodi ritornato un oggetto di tipo Measurement:

    public interface CanMeasurePM10
    {
        public Measurement GetPM10Value();
    }

    public interface CanMeasureCO
    {
        public Measurement GetCOValue();
    }
LabVIEW | Interfacce CanMeasurePM10 e CanMeasureCO.

Come da progettazione, la classe Sensor definisce le proprietà Name, SerialNumber e LastCalibration e il metodo Calibrate:

    public class Sensor
    {
        public string Name { get; set; }
        public string SerialNumber { get; set; }
        public DateTime LastCalibration { get; set; }

        public Sensor(string serialNumber, string name)
        {
            this.SerialNumber = serialNumber;
            this.Name = name;
        }

        public virtual void Calibrate()
        {
            // Calibrate
            this.LastCalibration = DateTime.Now;
        }
    }
LabVIEW | Classe Sensor.

Concludiamo l’implementazione della parte che riguarda i sensori con le due classi derivate PM10Sensor e COSensor. Le due implementano rispettivamente l’interfaccia CanMeasurePM10 e CanMeasureCO:

    public class PM10Sensor : Sensor, CanMeasurePM10
    {
        public PM10Sensor(string serialNumber, string name) :
                   base(serialNumber, name) { }

        public Measurement GetPM10Value()
        {
            return new Measurement(new Random().Next(0,100),"ug/m3", this.Name);
        }
    }

    public class COSensor : Sensor, CanMeasureCO
    {
        public COSensor(string serialNumber, string name) :
                    base(serialNumber, name) { }

        public Measurement GetCOValue()
        {
            return new Measurement(new Random().Next(0, 60), "mg/m3", this.Name);
        }
    }
LabVIEW | Classe PM10Sensor.
LabVIEW | Classe COSensor.

Passiamo ora all’implementazione della classe AirMonitoringStation e figlie.
La classe astratta AirMonitoringStation è un semplice contenitore degli attributi Location e Name:

    public abstract class AirMonitoringStation
    {
        public string Location { get; set; }
        public string StationName { get; set; }

        protected AirMonitoringStation(string location, string stationName)
        {
            this.Location = location;
            this.StationName = stationName;
        }
    }
LabVIEW | Classe AirMonitoringStation.

Come da progettazione, deriviamo due classi da AirMonitoringStation: MainStation e PeripheralStation.
La classe PeripheralStation, oltre ad essere derivata da AirMonitoringStation, implementa le due interfacce definite usate anche per i sensori (CanMeasurePM10 e CanMeasureCO):

    public class PeripheralStation : AirMonitoringStation, CanMeasurePM10, CanMeasureCO
    {
        public PeripheralStation(string location, string stationName) :
                   base(location, stationName) { }

        public Measurement GetCOValue()
        {
            return new Measurement(new Random().Next(0, 60), "mg/m3", this.StationName);
        }

        public Measurement GetPM10Value()
        {
            return new Measurement(new Random().Next(0, 100), "ug/m3", this.StationName);
        }
    }
LabVIEW | Classe PeripheralStation.

La classe MainStation invece è un po’ più complessa. Come definito in progettazione, la classe dipende dalle interfacce CanMeasurePM10 e CanMeasureCO e ha due metodi distinti per la lettura rispettivamente di PM10 e CO:

public class MainStation : AirMonitoringStation
    {
        public MainStation(string location, string stationName) : base(location, stationName) { }
        public List<CanMeasureCO>? COSources { get; set; }
        public List<CanMeasurePM10>? PM10Sources { get; set; }
        public List<Measurement>? GetPM10Values()
        {
            if (this.PM10Sources is null)
            {
                return null;
            }
            var measurementList = new List<Measurement>();
            foreach (var item in this.PM10Sources)
            {
                measurementList.Add(item.GetPM10Value());
            }
            return measurementList;
        }
        public List<Measurement>? GetCOValues()
        {
            if (this.COSources is null)
            {
                return null;
            }
            var measurementList = new List<Measurement>();
            foreach (var item in this.COSources)
            {
                measurementList.Add(item.GetCOValue());
            }
            return measurementList;
        }
    }
LabVIEW | Classe MainStation.

Passiamo infine alla parte di logging creando l’interfaccia DataLogger che definisce il metodo LogData. Il metodo LogData riceve in ingresso il parametro stringa data:

    public interface DataLogger
    {
        public void LogData(string data);
    }
LabVIEW | Interfaccia DataLogger.

L’interfaccia DataLogger è soddisfatta dalla classe ConsoleLogger responsabile di scrivere su console il dato da loggare. Si noti che per la versione LabVIEW, dove l’output su console non è nativamente supportato, la console è simulata scrivendo in un indicatore stringa.

    public class ConsoleLogger : DataLogger
    {
        public void LogData(string data)
        {
            Console.WriteLine($"New record: {data}");}
        }
    }
LabVIEW | Classe ConsoleLogger.

Abbiamo poi la classe LoggerManager che gestisce l’operazione di logging. La classe non dipende da classi concrete ma dall’interfaccia DataLogger. Tramite il costruttore (o il metodo Create per LabVIEW) si inietta l’oggetto concreto (nel nostro caso l’oggetto ConsoleLogger).

    public class DataLoggerManager
    {
        private DataLogger logger;

        public DataLoggerManager(DataLogger logger)
        {
            this.logger = logger;
        }

        public void LogData(string data)
        {
            this.logger.LogData(data);
        }
    }
LabVIEW | Classe LoggerManager.

Concludiamo l’esempio creando l’applicazione principale che mette assieme tutte le classi e interfacce create sopra.
Il programma inizializza gli oggetti di tipo MainStation, PeripheralStation, PM10Sensor, COSensor e ConsoleLogger. Quest’ultimo è poi iniettato nell’oggetto di tipo LoggerManager.
La lista di sorgenti PM10 per l’oggetto MainStation è composta dal sensore PM10Sensor e dall’oggetto PeripheralStation. Analogamente, la lista delle sorgenti di dati CO è composta dal sensore COSensor e di nuovo dall’oggetto PeripheralStation.
Infine, ad intervalli regolari, il programma interroga le sorgenti di PM10 e di CO e ne logga i dati tramite l’oggetto LoggerManager.

    internal class Program
    {
        static void Main(string[] args)
        {
            // Initialization of Stations
            var mainStation = new MainStation(location: "location 1", "Main Station");
            var peripheralStation = new PeripheralStation(location: "location 2", "Peripherical Station 1");
            // Initialization of Sensors
            var myPM10Sensor = new PM10Sensor(serialNumber: "1234","PM10 sensor 1");
            var myCOSensor = new COSensor(serialNumber: "ABCD", "CO sensor 1");
            // Initalization of the LoggerManager obj
            var loggerManager = new DataLoggerManager(new ConsoleLogger());
            // Assignement of PM10 and CO sources to the mainStation
            mainStation.PM10Sources = new List<CanMeasurePM10> { myPM10Sensor, peripheralStation };
            mainStation.COSources = new List<CanMeasureCO> { myCOSensor, peripheralStation };
            while (!Console.KeyAvailable)
            {
                // Return and log PM10 values
                foreach (var measurement in mainStation.GetPM10Values())
                {
                    loggerManager.LogData($"{measurement.Source} - {measurement.Value} {measurement.Unit}");
                }
                // Return and log CO values
                foreach (var measurement in mainStation.GetCOValues())
                {
                    loggerManager.LogData($"{measurement.Source} - {measurement.Value} {measurement.Unit}");
                }
                Thread.Sleep(1000);
            }
        }
    }
LabVIEW | Main.

Ed ecco qui l’esempio funzionante:

C# | Applicazione in funzione
LabVIEW | Applicazione in funzione.

Con questo esempio si conclude il discorso sulle interfacce e i loro utilizzi. Grazie per il tempo dedicato alla lettura di questo articolo. Mi auguro che i concetti esposti siano sufficientemente chiari e possano essere d’aiuto nel risolvere problemi reali. Nel caso di dubbi, richieste o semplici curiosità, sono disponibile qui.
Lascio qui sotto il link per il download del progetto LabVIEW (versione 2023) e qualche link con altri esempi nell’uso di interfacce (per LabVIEW e non solo).
Al prossimo post!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Scroll to Top