Progettazione OO per un'applicazione Windows che comunica con un computer esterno tramite RS232

3

Ho seguito un po 'di consigli di progettazione OO ... Sto per iniziare a sviluppare un'applicazione Windows che comunica con una macchina esterna tramite RS232. La macchina dispone di un "controller di sistema" integrato costituito da registri (indirizzi). È possibile scrivere un valore in un registro (ad es. Per accendere una pompa) o leggere un valore da un registro (ad esempio per vedere se la pompa è in funzione). L'app avrà un file di configurazione che descrive le impostazioni di ciascun registro, ad es. il suo indirizzo, che sia 16 o 32 bit, un "alias" amichevole (quindi un altro codice può gestire "Pump XYZ" piuttosto che l'indirizzo "50B3D124") e alcune altre cose.

Il mio primo pensiero è stato quello di avere una classe Register che riflette queste proprietà, oltre a Read() e Write() metodi. Ma questo solleva la prima domanda - il metodo Read() ha un valore di ritorno, o dovrebbe compilare una proprietà Value sulla classe?

Allo stesso modo, se il metodo Write() include un parametro "valueToWrite", o dovrebbe scrivere qualsiasi valore attualmente detenuto nella proprietà Value ?

Potrei fare un ulteriore passo avanti (possibilmente anche rispondendo alla domanda precedente) - invece dei metodi Read / Write, metti la funzionalità nel getter e setter della proprietà Value ? L'utilizzo di una proprietà per questo scopo non sembra giusto.

Poi ho iniziato a chiedermi se fosse necessaria una classe Register , poiché la sua funzionalità di lettura / scrittura farebbe poco più che chiamare i metodi Read / Write su una classe SystemController , ad es.

long ReadRegister(string registerAlias);
void WriteRegister(string registerAlias, long value);

Perché non lasciare il resto dei registri di lettura / scrittura all'applicazione tramite la classe SystemController ? (Sospetto che avrei ancora bisogno di un concetto di una classe Register , anche se è solo per rappresentare le impostazioni di configurazione di un registro).

    
posta Andrew Stephens 21.02.2014 - 09:39
fonte

2 risposte

4

But this raises the first question - should the Read() method have a return value, or should it populate a Value property on the class? Similarly, should the Write() method include a "valueToWrite" parameter, or should it write whatever value is currently held in the Value property?

Se utilizzi una proprietà Value invece di restituire un valore da Read() , puoi "scrivere" valori su Register tramite Write() , ma continua a vedere un vecchio valore finché non chiami Read() . hai introdotto una mutazione non necessaria nella tua implementazione e, soprattutto, non è così che funzionano i registri hardware.

La tua classe Register dovrebbe essere immutabile, memorizzare solo la quantità minima di informazioni necessarie per accedere al registro hardware reale (probabilmente solo l'indirizzo) e nascondere tali informazioni dal mondo esterno. L'unica cosa che dovresti essere in grado di fare con una variabile Register è leggere, scrivere e controllare se è lo stesso registro hardware di un altro Register . Ad esempio, in Java:

public final class Register<T extends Number> {
    public static final Register<Short> PUMP_XYZ = new Register<Short>(0xA3F990);
    public static final Register<Integer> PUMP_ABC = new Register<Integer>(0xA3F9A0);
    // and so on

    private final int address;

    private Register(int address) {
        this.address = address;
    }

    public T read() throws CommunicationException {
        return /* read hardware */
    }

    public void write(T value) throws CommunicationException {
        /* write to hardware */
    }

    @Override
    public boolean equals(Object other) {
        if (other instanceof Register) {
            return this.address == ((Register) other).address;
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return address;
    }

    @Override
    public String toString() {
        return String.format("Register(0x%h)", address);
    }

    public static final class CommunicationException extends RuntimeException {}
}

Questa è l'implementazione più semplice che cattura tutti i dettagli essenziali di un vero registro. È anche immutabile e non consente l'ereditarietà, che dovresti preferire quando possibile.

Si sta tentando di allegare un nome stampabile a Register , ma questo non fa realmente parte di ciò che stai cercando di modellare. Un registro hardware non ha la nozione di un nome. Sarebbe meglio tenerlo separato e creare un Map<Register, String> o una semplice struttura che contiene un campo String e Register .

Why not just let the rest of the application read/write registers via the SystemController class?

Un giorno potresti cambiare il protocollo di comunicazione come USB o usare un'API diversa per effettuare le chiamate RS-232. L'applicazione non si cura di come i registri sono accessibili e non dovrebbero sapere; è un dettaglio di implementazione.

    
risposta data 21.02.2014 - 22:35
fonte
1

Ci sono diversi approcci che possono funzionare; il modo migliore può dipendere dalla velocità e dall'affidabilità del protocollo di comunicazione, dalla natura delle operazioni e dalla misura in cui l'uso del programma "fluirà" meglio se gli utenti attendono che ciascuna richiesta venga completata prima di procedere alla successiva, immettere richiede il più velocemente possibile e fa in modo che il programma li gestisca il più velocemente possibile oppure che gli utenti inseriscano tutte le richieste e le elaborino come gruppo.

Se si desidera che l'interfaccia utente aspetti mentre ciascuna operazione viene eseguita, avere metodi che eseguono semplicemente le operazioni direttamente ma non mantengono nessuno stato potrebbe essere il modo più semplice per farlo. Un simile approccio è semplice e generalmente rende facile capire cosa sta succedendo. Se la parte remota della connessione presenta alcuni aspetti importanti dello stato che influiscono sul modo in cui verranno eseguite le comunicazioni (ad esempio, impostazioni della modalità di comunicazione o numeri di sequenza dei pacchetti), può essere utile che la classe gestisca tali aspetti dello stato stesso, ma aspetti dello stato diversi dalla comunicazione dovrebbero essere gestiti dal codice client.

Un design alternativo è quello di avere metodi "read all state" e "write all state", e il flusso dell'interfaccia richiede lo stato di caricamento da un dispositivo o la scelta di creare un nuovo stato, modificando lo stato senza coinvolgere il dispositivo, e quindi memorizzando lo stato sul dispositivo una volta completata la modifica. Questo approccio ridurrà al minimo il numero di volte in cui un utente dovrà attendere il dispositivo, consolidando tutti i ritardi in un'unica attesa all'inizio e una alla fine. L'unica limitazione con questo approccio è che l'utente non sarà in grado di vedere l'effetto di eventuali modifiche sul dispositivo remoto fino a quando non viene premuto il pulsante "applica" (poiché non verranno apportate modifiche fino a quel momento). Che questa sia una cosa buona o cattiva dipenderà dalla natura del dispositivo.

Un ultimo approccio consiste nel mantenere due insiemi principali di stato: ciò che il dispositivo ha segnalato per ultimo e quali modifiche (se esistenti) sono state richieste ma non ancora applicate; insieme a quello si potrebbe desiderare di tenere traccia di quanti tentativi non riusciti sono stati fatti per eseguire varie operazioni. Uno potrebbe per ogni proprietà del dispositivo visualizzare un campo di sola lettura e modificabile uno accanto all'altro. Un'attività in background deve eseguire la scansione delle proprietà e determinare quali elementi devono essere aggiornati sullo schermo o nel dispositivo ed eseguire gli aggiornamenti man mano che li noti. Suggerirei di utilizzare le caselle di testo di sola lettura implementate con un tipo personalizzato con un metodo che può impostare il testo in qualsiasi momento da qualsiasi thread. Il metodo dovrebbe immediatamente impostare una variabile privata sul nuovo testo e BeginInvoke un metodo UpdateText quando nessuna richiesta di aggiornamento è già in sospeso . Se vengono eseguite più richieste per impostare il testo prima che il thread dell'interfaccia utente abbia avviato l'elaborazione del primo, le operazioni ripetute non dovrebbero generare più% chiamate di% di co_de. Invece, le operazioni successive dovrebbero semplicemente cambiare la stringa che verrà mostrata quando il metodo BeginInvoke inizierà finalmente l'esecuzione.

Una cosa importante da tenere a mente è che mentre le proprietà effettive del dispositivo fisico e le proprietà desiderate sarebbero idealmente la stessa cosa, sono aspetti separati di stato e probabilmente dovrebbero essere entrambi mantenuti e (specialmente con il terzo approccio) visualizzato come tale. Se per qualsiasi ragione, la lettura dello stato del dispositivo dopo averlo impostato non produce il nuovo valore, il codice dovrebbe essere pronto a trasmettere tali informazioni all'utente. L'utente potrebbe essere confuso sul motivo per cui il valore non viene aggiornato, ma sarà probabilmente meno confuso di quanto il software suggerisca che il dispositivo contenga il nuovo valore ma in realtà non lo fa.

    
risposta data 24.02.2014 - 17:31
fonte

Leggi altre domande sui tag