Accoppiamento lento in design orientato agli oggetti

16

Sto cercando di imparare GRASP e ho trovato questo spiegato ( qui a pagina 3 ) su Low Coupling e sono rimasto molto sorpreso quando ho trovato questo:

Consider the method addTrack for an Album class, two possible methods are:

addTrack( Track t )

and

addTrack( int no, String title, double duration )

Which method reduces coupling? The second one does, since the class using the Album class does not have to know a Track class. In general, parameters to methods should use base types (int, char ...) and classes from the java.* packages.

Tendo a diasgree con questo; Credo che addTrack(Track t) sia migliore di addTrack(int no, String title, double duration) a causa di vari motivi:

  1. È sempre meglio per un metodo avere il minor numero possibile di parametri (secondo il Codice pulito di Uncle Bob nessuno o uno preferibilmente, 2 in alcuni casi e 3 in casi speciali; più di 3 necessita di refactoring - questi sono di Raccomandazioni sui corsi non sulle regole dell'agrifoglio).

  2. Se addTrack è un metodo di un'interfaccia e i requisiti richiedono che un Track debba avere più informazioni (ad esempio anno o genere), l'interfaccia deve essere modificata e il metodo deve supportare un altro parametro.

  3. L'incapsulamento è rotto; se addTrack è in un'interfaccia, allora non dovrebbe conoscere le parti interne di Track .

  4. In realtà è più accoppiato nel secondo modo, con molti parametri. Supponiamo che il parametro no debba essere cambiato da int a long perché ci sono più di MAX_INT tracce (o per qualsiasi ragione); quindi sia il Track che il metodo devono essere modificati mentre se il metodo sarebbe addTrack(Track track) solo la Track verrebbe modificata.

Tutti e 4 gli argomenti sono effettivamente connessi tra loro, e alcuni di essi sono conseguenze di altri.

Quale approccio è migliore?

    
posta m3th0dman 27.06.2013 - 17:40
fonte

6 risposte

15

Bene, i tuoi primi tre punti riguardano in realtà altri principi piuttosto che l'accoppiamento. Devi sempre trovare un equilibrio tra i principi di design spesso in conflitto.

Il tuo quarto punto è sull'accoppiamento e sono assolutamente d'accordo con te. L'accoppiamento riguarda il flusso di dati tra i moduli. Il tipo di contenitore in cui i dati entrano è in gran parte irrilevante. Passare una durata come doppio anziché come campo di Track non elimina la necessità di superarlo. I moduli devono ancora condividere la stessa quantità di dati e avere ancora la stessa quantità di accoppiamento.

Inoltre, non considera tutti gli accoppiamenti nel sistema come un aggregato. Mentre l'introduzione di una classe Track aggiunge certamente un'altra dipendenza tra due singoli moduli, può ridurre significativamente l'accoppiamento del sistema , che è la misura importante qui.

Ad esempio, considera un pulsante "Aggiungi alla playlist" e un oggetto Playlist . È possibile considerare l'introduzione di un oggetto Track per aumentare l'accoppiamento se si considerano solo questi due oggetti. Ora hai tre classi interdipendenti invece di due. Tuttavia, non è l'insieme del tuo sistema. È inoltre necessario importare la traccia, riprodurre la traccia, visualizzare la traccia, ecc. L'aggiunta di un'altra classe a quel mix è trascurabile.

Considerare ora la necessità di aggiungere il supporto per riprodurre le tracce sulla rete anziché solo localmente. Devi solo creare un oggetto NetworkTrack conforme alla stessa interfaccia. Senza l'oggetto Track , dovresti creare funzioni ovunque come:

addNetworkTrack(int no, string title, double duration, URL location)

Questo raddoppia efficacemente il tuo accoppiamento, richiedendo anche moduli che non si preoccupano delle cose specifiche della rete, ma comunque ne tengono traccia, per poterlo trasmettere.

Il tuo test effetto a catena è buono per determinare la tua reale quantità di accoppiamento. Ciò che ci preoccupa è limitare i luoghi in cui influisce un cambiamento.

    
risposta data 27.06.2013 - 20:07
fonte
10

La mia raccomandazione è:

Usa

addTrack( ITrack t )

ma assicurati che ITrack sia un'interfaccia e non una classe concreta.

L'album non conosce gli interni di ITrack implementors. È solo abbinato al contratto definito da ITrack .

Penso che questa sia la soluzione che genera il minor numero di accoppiamenti.

    
risposta data 27.06.2013 - 18:19
fonte
4

Direi che il secondo metodo di esempio è probabilmente l'accoppiamento aumenta , dal momento che molto probabilmente istanzia un oggetto Traccia e lo memorizza nell'oggetto Album corrente. (Come suggerito nel mio commento sopra, suppongo che sia intrinseco che una classe Album abbia il concetto di una classe Track da qualche parte al suo interno.)

Il primo metodo di esempio presuppone che una traccia venga istanziata al di fuori della classe Album, quindi, per lo meno, possiamo supporre che la istanza della classe Track non sia accoppiata alla classe Album.

Se le best practice hanno suggerito che non abbiamo mai un riferimento di classe a una seconda classe, la totalità della programmazione orientata agli oggetti sarebbe buttata fuori dalla finestra.

    
risposta data 27.06.2013 - 18:14
fonte
3

L'accoppiamento è solo uno dei tanti aspetti da provare per ottenere nel tuo codice. Riducendo l'accoppiamento, non stai necessariamente migliorando il tuo programma. In generale, questa è una best practice, ma in questa particolare istanza, perché non si dovrebbe conoscere Track ?

Usando una classe Track da passare a Album , stai rendendo il tuo codice più facile da leggere, ma ancora più importante, come hai detto, stai trasformando un elenco statico di parametri in un oggetto dinamico. Questo alla fine rende la tua interfaccia molto più dinamica.

Hai detto che l'incapsulamento è rotto, ma non lo è. Album deve conoscere gli interni di Track , e se non hai usato un oggetto, Album dovrebbe conoscere ogni singola informazione passata prima che possa farne lo stesso. Il chiamante deve conoscere anche le parti interne di Track , poiché deve costruire un oggetto Track , ma il chiamante deve sapere tutte queste informazioni allo stesso modo se è passato direttamente al metodo. In altre parole, se il vantaggio dell'incapsulamento non è conoscere il contenuto di un oggetto, non può essere usato in questo caso poiché Album deve utilizzare le informazioni di Track allo stesso modo.

Dove non si desidera utilizzare Track è se Track contiene una logica interna a cui non si desidera che il chiamante abbia accesso. In altre parole, se Album fosse una classe utilizzata da un programmatore che utilizza la tua libreria, non vorrai che utilizzi Track se lo usi per dire, chiama un metodo per mantenerlo nel database. Il vero problema con questo risiede nel fatto che l'interfaccia è impigliata con il modello.

Per risolvere il problema, devi separare Track nei suoi componenti di interfaccia e i suoi componenti logici, creando due classi separate. Per il chiamante, Track diventa una classe leggera che ha lo scopo di contenere informazioni e offrire ottimizzazioni minori (dati calcolati e / o valori predefiniti). All'interno di Album , si utilizzerà una classe denominata TrackDAO per eseguire il sollevamento pesante associato al salvataggio delle informazioni da Track nel database.

Naturalmente, questo è solo un esempio. Sono sicuro che non è affatto il tuo caso e quindi sentiti libero di usare Track guilt-free. Ricorda solo di tenere a mente il chiamante quando stai costruendo classi e per creare interfacce quando richiesto.

    
risposta data 27.06.2013 - 18:13
fonte
3

Entrambi sono corretti

addTrack( Track t ) 

è migliore (come hai già argomentato) mentre

addTrack( int no, String title, double duration ) 

è less coupled perché il codice che utilizza addTrack non ha bisogno di sapere che esiste una classe Track . La traccia può essere rinominata ad esempio senza la necessità di aggiornare il codice chiamante.

Mentre parli di codice più leggibile / gestibile, l'articolo parla di accoppiamento . Il codice meno accoppiato non è necessariamente più facile da implementare e comprendere.

    
risposta data 27.06.2013 - 19:10
fonte
3

Low Coupling non significa No Coupling. Qualcosa, da qualche parte, deve conoscere oggetti altrove nella base di codice, e più riduci la dipendenza da oggetti "personalizzati", più motivi ti danno per cambiare il codice. Ciò che l'autore cita sta promuovendo con la seconda funzione è meno accoppiato, ma anche meno orientato agli oggetti, che è contrario all'intera idea di GRASP come una metodologia di progettazione orientata agli oggetti . L'intero punto è come progettare il sistema come una collezione di oggetti e le loro interazioni; evitarli è come insegnarti a guidare una macchina dicendo che dovresti andare in bici.

Invece, la strada giusta è ridurre la dipendenza dagli oggetti concreti , che è la teoria di "accoppiamento lento". Il minor numero di tipi di calcestruzzo definito di cui un metodo deve avere conoscenza, meglio è. Proprio in base a questa affermazione, la prima opzione è in realtà meno accoppiata, perché il secondo metodo che accetta i tipi più semplici deve conoscere tutti questi tipi più semplici. Certo, sono integrati e il codice all'interno del metodo potrebbe dover essere curato, ma la firma del metodo e dei chiamanti del metodo è sicuramente non . Cambiare uno di questi parametri relativi a una traccia audio concettuale richiederà più cambiamenti quando sono separati rispetto a quando sono contenuti in un oggetto Traccia (che è il punto degli oggetti, l'incapsulamento).

Andando un passo avanti, se ci si aspettava che la traccia fosse sostituita con qualcosa che facesse lo stesso lavoro, forse un'interfaccia che definisse la funzionalità richiesta sarebbe in ordine, un ITrack. Ciò potrebbe consentire implementazioni differenti come "AnalogTrack", "CdTrack" e "Mp3Track" che hanno fornito informazioni aggiuntive più specifiche per tali formati, pur continuando a fornire un'esposizione di base di ITrack che rappresenta concettualmente una "traccia"; un sotto-pezzo finito di audio. La traccia potrebbe essere analogamente una classe base astratta, ma questo richiede che tu voglia sempre utilizzare l'implementazione inerente alla traccia; reimplementalo come BetterTrack e ora devi modificare i parametri previsti.

Quindi la regola d'oro; i programmi e i loro componenti di codice sempre hanno motivi per cambiare. Non puoi scrivere un programma che mai richiede la modifica del codice che hai già scritto per aggiungere qualcosa di nuovo o modificare il suo comportamento. Il tuo obiettivo, in qualsiasi metodologia (GRASP, SOLID, qualsiasi altro acronimo o parola d'ordine che puoi pensare) è semplicemente identificare le cose che dovranno cambiare nel tempo e progettare il sistema in modo che tali cambiamenti sono facili da realizzare quanto più possibile (tradotto, toccando poche righe di codice e influenzando il minor numero possibile di altre aree del sistema oltre l'ambito del cambiamento previsto). Caso in questione, ciò che è più probabile che cambi è che una Traccia guadagnerà più membri di dati a cui addTrack () può o non potrebbe importare, non che la traccia sarà sostituita con BetterTrack.

    
risposta data 27.06.2013 - 19:33
fonte