Coerenza di un oggetto

1

Tendo a mantenere gli oggetti coerenti durante la loro vita. In alcuni casi, l'impostazione di un oggetto richiede più chiamate a routine diverse. Ad esempio, un oggetto di connessione può operare in questo modo:

Connection c = new Connection();
c.setHost("http://whatever")
c.setPort(8080)
c.connect()

per favore nota che questo è solo un esempio stupido per farti capire il punto. Tra le chiamate a setHost e setPort l'oggetto è incoerente, perché la Porta non è stata ancora specificata, quindi questo codice si arresterebbe in modo anomalo

Connection c = new Connection();
c.setHost("http://whatever")
c.connect()

Significa che è necessario che connect () abbia chiamate precedenti sia a setHost che a setPort, altrimenti non sarà in grado di funzionare in quanto il suo stato è incoerente.

È possibile risolvere il problema con un valore predefinito, ma potrebbero esserci casi in cui non è possibile definire alcun valore predefinito. Nell'esempio successivo assumiamo che non esiste un valore predefinito per la porta e quindi una chiamata a c.connect () senza prima chiamare sia setHost che setPort sarà uno stato incoerente dell'oggetto. Questo, per me, indica un design dell'interfaccia scorretta, ma potrei sbagliarmi, quindi voglio sentire la tua opinione.

Organizzate la vostra interfaccia in modo che l'oggetto sia sempre in uno stato coerente (cioè lavorabile) sia prima che dopo la chiamata?

Modifica : non tentare di risolvere il problema che ho esposto sopra. So come risolverlo. La mia domanda è molto più ampia nel senso. Sto cercando un principio di progettazione, dichiarato ufficialmente o informalmente, riguardante la coerenza dello stato dell'oggetto tra le chiamate.

    
posta Stefano Borini 13.01.2011 - 12:02
fonte

11 risposte

3

Credo di capire quello che stai cercando di ottenere, e ci sono alcuni approcci diversi che puoi usare. Gli stati incoerenti creano un problema nelle applicazioni di rientro multithreading.

Approccio all'invio delle dipendenze

Probabilmente la minima quantità di lavoro con il massimo ritorno sull'investimento sarebbe quella di accoppiare i valori connessi ogni volta che vengono cambiati. Usando il tuo esempio, avremmo il codice che assomiglia a questo:

// when we initialize the object
Connection conn = new Connection("http://someserver", 8080);

// when we want to change something:
conn.SetDestination("http://google.com", 80);

Approccio per l'interfaccia fluida / generosa

Il book GoF Design Patterns delinea un pattern chiamato builder e alcune persone ora li chiamano "Fluent Interfaces". Essenzialmente un costruttore è una classe di fabbrica di oggetti la cui unica responsabilità è quella di raccogliere tutti i dati prima di creare l'oggetto desiderato. Il costruttore è sempre coerente con se stesso per quanto riguarda il problema che sta risolvendo. È un oggetto separato da quello che stai cercando di creare. L'utilizzo di un builder sarebbe simile a questo:

Connection conn = new ConnectionBuilder().SetServer("http://someserver")
    .SetPort(8080).Connect();

Questo approccio può essere alquanto dettagliato, ma l'istanza di Connection è sempre coerente. Se Connection è immutable (un'altra buona caratteristica di design per oggetti di questo tipo), allora non c'è modo di metterlo in uno stato non valido.

Accetta incoerenza

So che sembra un'eresia considerando la tua domanda, ma ti costringe a pensare in modo diverso a come progettare la tua applicazione. Ammettiamolo, anche con l'approccio di iniezione delle dipendenze, si avrà un punto all'interno del metodo SetDestination() in cui l'oggetto sarà temporaneamente incoerente. È possibile impostare solo una variabile alla volta. Quando esci dal metodo, l'oggetto è di nuovo coerente.

Quindi il punto di questo approccio è di non farsi piegare attorno all'asse quando si ha un oggetto che potrebbe non essere coerente fino a quando non viene chiamato un metodo finale. L'esempio che hai fornito funziona per dimostrare questo approccio.

Connection conn = new Connection();
conn.SetServer("http://someserver");
conn.SetPort(8080);

// point where consistency matters
conn.Connect();

Con questo ultimo approccio, ti viene richiesto di scrivere il codice di controllo degli errori per assicurarti che tutte le precondizioni di conn.Connect() siano soddisfatte prima di eseguire effettivamente il lavoro. Questa è una buona cosa - dovresti farlo comunque. I JavaBeans sono progettati con questo approccio nel bene e nel male. Il vantaggio è che è possibile associare l'oggetto a un'interfaccia utente ed eseguire il codice critico dopo aver impostato le informazioni pertinenti, anziché compilare un solo oggetto per copiare i valori su un altro oggetto.

    
risposta data 13.01.2011 - 18:19
fonte
5

Che ne dici di passare i valori all'inizializzatore?

Connection c = new Connection("http://foo", 80);
c.connect();

Personalmente, uso questo approccio quando codifico con ruby (e rail) e Objective-C (e Cocoa).

Aggiornamento: cerco di passare tutti i valori non predefiniti nell'inizializzatore dell'oggetto. In questo modo ho un oggetto coerente e funzionante fin dall'inizio (cioè l'inizializzazione).

    
risposta data 13.01.2011 - 12:21
fonte
4

Ci sono alcune interfacce che non mantengono questa coerenza come l'API OpenGL, che usa macchine a stati. Puoi aspettarti che il codice crei un triangolo come:

glBegin(GL_TRIANGLES);

glVertex3f(-1.0f, -0.5f, -4.0f);    // lower left vertex
glVertex3f( 1.0f, -0.5f, -4.0f);    // lower right vertex
glVertex3f( 0.0f,  0.5f, -4.0f);    // upper vertex

glEnd();

Ovviamente non c'è nulla che ti impedisca di scrivere codice come:

glEnd();
glEnd();
glBegin();
glVertex3f(0.0f, 0.0f, 0.0f);

Direi che è difficile se non impossibile sostenere che qualsiasi interfaccia può essere resa coerente. Prendi le transazioni del database, per esempio. Non si scriverebbe un'interfaccia a un database creando un metodo che apre una transazione, aggiorna un record e impegna la transazione in caso di successo, altrimenti il rollback. Se lo hai fatto, stai escludendo la possibilità di fare due aggiornamenti atomici all'interno di una transazione piuttosto che una sola. Quindi dovresti creare ancora un altro metodo.

È meglio avere un modello di macchina a stati. Permetti al programmatore di rovinare usando un modello del genere, ma secondo me, fino a quando ogni open ha una chiusura, ogni connessione ha una disconnessione e fintanto che qualsiasi tentativo di eseguire un'azione al di fuori del suo stato genera un'eccezione, Penso che sia perfettamente accettabile.

Nel tuo particolare esempio, direi che non è appropriato usare un motore di macchina statale semplicemente perché non ha alcuno scopo. In generale, dovresti essere il più coerente possibile, purché soddisfi le tue esigenze.

    
risposta data 13.01.2011 - 12:48
fonte
2

Come sottolinea Bart van Ingen Schenau nel suo commento ci sono tre casi:

Caso 1: l'utente non fornisce tutte le informazioni richieste.

Questo è gestito da ciò che suggerisce Eimantas e vorrei approvare questo approccio, devi ancora inserire il codice nei tuoi metodi che controllano lo stato dell'oggetto in quanto non vi è alcuna garanzia che l'utente abbia inserito dati validi, ovvero

Caso 2: l'utente fornisce dati fittizi.

Ad esempio, potresti avere:

Connection c = new Connection("hello, world", 0);
c.connect();

che fallirebbe ancora.

Hai quindi bisogno del codice per controllare lo stato e generare un errore o un'eccezione se l'oggetto non è in uno stato valido.

Caso 3: l'utente fornisce dati plausibili.

Questo è più difficile da controllare e di solito richiede che i metodi abbiano una gestione degli errori adeguata al loro interno. Ad esempio, potresti avere:

Connection c = new Connection("http://www.invaliddomain.com", 80);
c.connect();

Sembra OK, ma senza effettuare la chiamata non puoi sapere se i dati sono validi o meno.

    
risposta data 13.01.2011 - 12:55
fonte
2

ci si aspetta che un oggetto sia in uno stato coerente tra le chiamate di metodo

il problema che pensi di avere non esiste.

prima che le fiamme e i downvotes inizino, considera questo: hai un oggetto con due proprietà X e Y che devono essere uguali. In assenza di una vera assegnazione parallela, non è possibile assegnare un nuovo valore a entrambe le proprietà in modo tale che il vincolo "deve essere uguale" non sia temporaneamente violato.

    
risposta data 13.01.2011 - 16:55
fonte
2

Sono abbastanza sorpreso che nessuno l'abbia ancora inventato.

Gli oggetti non inizializzati o quasi inizializzati sono una cosa cattiva (tm), rendono il codice inutilmente fragile. In effetti, è sempre uno dei miei obiettivi principali, durante una revisione del codice, scoprire le situazioni che "funzionano ora" ma richiedono estrema attenzione per continuare a lavorare ogni volta che viene eseguita una manutenzione sul codice.

Quando un oggetto è costruito, dovrebbe essere utilizzabile. Qualsiasi altro stato è eresia. Non è nemmeno particolarmente difficile, quindi non ha molto senso non farlo bene.

Il primo consiglio che ho visto, che è un passo nella giusta direzione, è raggruppare il parametro host e port in un singolo setter e inizializzarli direttamente dal costruttore. È un buon consiglio.

Connection conn = Connection('localhost', '8080')
conn.Connect()

Potresti notare, tuttavia, che ci sono ancora due passaggi da compiere prima che si possa effettivamente usare la connessione e che nel medio termine ci sia un oggetto "Quasi inizializzato".

A questo punto si potrebbe giustamente obiettare che se leggiamo i dettagli da qualche file di configurazione o qualsiasi altro mezzo, potremmo non essere in grado di connetterci al momento in cui li leggiamo, e quindi dobbiamo solo accettarlo.

Non.

Se abbiamo due diversi stati di cose, significa solo che abbiamo bisogno di due oggetti diversi:

ConnectionDescription connDesc = ConnectionDescription('localhost', '8080')

connDesc.Set('localhost', '80') // oups, the port was wrong

Connection conn = Connection(connDesc)

E basso ed ecco: abbiamo solo oggetti perfettamente coerenti!

Nota: per motivi di sicurezza, l'elemento Connection dovrebbe fare una copia della descrizione.

    
risposta data 14.01.2011 - 08:28
fonte
1

Un modo sarebbe utilizzare un costruttore:

ConnectionInterface c = new ConnectionImpl("http://whatever", 8080);
c.connect();

Ma poi sei legato all'implementazione, dal momento che le interfacce non possono avere costruttori.

    
risposta data 13.01.2011 - 12:45
fonte
1

Potresti usare una Fluent Interface aiutarti?

Il tuo esempio sarebbe quindi:

Connection c = new Connection()
    .setHost("http://whatever")
    .setPort(8080)
    .connect();

Come descritto nell'articolo wiki, ogni metodo di un'interfaccia fluente restituisce l'istanza corrente. Non vedo perché non è possibile restituire il cast dell'istanza a un'interfaccia specifica basata su determinati valori di variabile che a sua volta determina quali funzioni sono disponibili in quel punto. Pertanto, puoi restituire l'istanza come un'interfaccia che consente di chiamare connect() dopo aver impostato host e port .

    
risposta data 13.01.2011 - 13:06
fonte
0

Vorrei ridisegnare l'interfaccia, qualcosa come

Connection c = new Connection();
c.connect("http://whatever", 8080)

Questo assicura che i valori necessari siano presenti quando chiami quel metodo.

Anch'io penso che gli stati incoerenti dovrebbero essere evitati il più possibile.

    
risposta data 13.01.2011 - 12:09
fonte
0

Il raggiungimento di uno stato coerente può essere effettuato in due modi: valori predefiniti e fabbriche di oggetti.

Puoi sempre fornire un valore predefinito. In caso di una connessione precedente, è sufficiente incorporare un piccolo server Web nell'app e rendere predefinito link . Lo stesso vale per altre situazioni. Devi solo scegliere un valore come predefinito e assicurarti che funzioni (anche come un manichino). A volte potrebbe richiedere uno sforzo assurdo quindi non è l'opzione migliore.

In secondo luogo, un approccio un po 'migliore è la fabbrica di oggetti. Dato un oggetto Connection di cui sopra con poche proprietà, è necessario conoscere effettivamente quali proprietà sono cruciali e quali sono opzionali. Quella conoscenza potrebbe essere incapsulata in una fabbrica di oggetti, che potrebbe garantire che sia possibile produrre un oggetto coerente o non ottenere alcun oggetto.

    
risposta data 13.01.2011 - 12:58
fonte
0

Mi piace lo stile in cui uno stato 'predefinito' di un oggetto è un 'nullo' o 'non valido'. la maggior parte delle operazioni in un oggetto non valido falliscono o ritornano senza effetti collaterali. Vuoi anche un metodo 'isValid' per controllarlo.

fa anche parte della tendenza 'never return NULL', restituisce solo un oggetto non valido della classe giusta. In questo modo, il codice di chiamata non deve sempre verificare i risultati NULL, poiché un oggetto non valido è innocuo e potrebbe essere "completato" in seguito.

    
risposta data 13.01.2011 - 13:21
fonte

Leggi altre domande sui tag