Metodo concatenamento vs incapsulamento


17

Esiste il classico problema OOP del concatenamento dei metodi rispetto ai metodi "single-access-point":

main.getA().getB().getC().transmogrify(x, y)

vs

main.getA().transmogrifyMyC(x, y)

Il primo sembra avere il vantaggio che ogni classe è responsabile solo di un insieme più piccolo di operazioni e rende tutto molto più modulare - l'aggiunta di un metodo a C non richiede alcuno sforzo in A, B o C per esporlo.

Il rovescio della medaglia, ovviamente, è l' incapsulamento più debole , che il secondo codice risolve. Ora A ha il controllo di ogni metodo che lo attraversa e può delegarlo ai suoi campi se lo desidera.

Mi rendo conto che non esiste un'unica soluzione e ovviamente dipende dal contesto, ma mi piacerebbe davvero sentire alcuni suggerimenti su altre importanti differenze tra i due stili e in quali circostanze dovrei preferire uno di essi - perché in questo momento, quando provo per progettare un po 'di codice, mi sento come se non stessi usando gli argomenti per decidere in un modo o nell'altro.

Risposte:


25

Penso che la Legge di Demetra fornisca una linea guida importante in questo (con i suoi vantaggi e svantaggi, che, come al solito, dovrebbe essere misurato in base al caso).

Il vantaggio di seguire la Legge di Demetra è che il software risultante tende ad essere più gestibile e adattabile. Poiché gli oggetti sono meno dipendenti dalla struttura interna di altri oggetti, i contenitori di oggetti possono essere modificati senza rielaborare i chiamanti.

Uno svantaggio della Legge di Demetra è che a volte è necessario scrivere un gran numero di piccoli metodi "wrapper" per propagare le chiamate di metodo ai componenti. Inoltre, l'interfaccia di una classe può diventare ingombrante poiché ospita metodi per classi contenute, risultando in una classe senza un'interfaccia coesiva. Ma questo potrebbe anche essere un segno di cattivo design OO.


Mi sono dimenticato di quella legge, grazie per avermelo ricordato. Ma quello che sto chiedendo qui è principalmente quali sono i vantaggi e gli svantaggi, o più precisamente come dovrei decidere di usare uno stile sull'altro.
Oak,

@Oak, ho aggiunto citazioni che descrivono i vantaggi e gli svantaggi.
Péter Török,

10

In generale, cerco di limitare il più possibile il concatenamento del metodo (basato sulla Legge di Demetra )

L'unica eccezione che faccio è per interfacce fluide / programmazione interna in stile DSL.

Martin Fowler fa una specie della stessa distinzione in Linguaggi specifici di dominio, ma per motivi di separazione delle query dei comandi di violazione che afferma:

che ogni metodo dovrebbe essere un comando che esegue un'azione o una query che restituisce dati al chiamante, ma non entrambi.

Fowler nel suo libro a pagina 70 dice:

La separazione comandi-query è un principio estremamente prezioso nella programmazione e incoraggio vivamente i team a utilizzarlo. Una delle conseguenze dell'uso del metodo di concatenamento nei DSL interni è che di solito si rompe questo principio: ogni metodo altera lo stato ma restituisce un oggetto per continuare la catena. Ho usato molti decibel denigrando le persone che non seguono la separazione tra query e comandi, e lo farò di nuovo. Ma le interfacce fluide seguono un diverso insieme di regole, quindi sono felice di permetterlo qui.


3

Penso che la domanda sia se stai usando un'astrazione adatta.

Nel primo caso, abbiamo

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Dove principale è di tipo IHasGetA. La domanda è: è adatta quell'astrazione? La risposta non è banale. E in questo caso sembra un po 'fuori posto, ma è comunque un esempio teorico. Ma per costruire un esempio diverso:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

È spesso meglio di

main.superTransmogrify(v, w, x, y, z);

Poiché in quest'ultimo esempio entrambi thise maindipende dal tipo di v, w, x, ye z. Inoltre, il codice non sembra molto migliore se ogni dichiarazione di metodo ha una mezza dozzina di argomenti.

Un localizzatore di servizi in realtà richiede il primo approccio. Non si desidera accedere all'istanza che crea tramite il localizzatore di servizi.

Quindi "raggiungere" attraverso un oggetto può creare molta dipendenza, tanto più se si basa sulle proprietà delle classi reali.
Tuttavia, la creazione di un'astrazione, che consiste nel fornire un oggetto, è una cosa completamente diversa.

Ad esempio, potresti avere:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Dov'è mainun'istanza di Main. Se la conoscenza della classe mainriduce la dipendenza IHasGetAanziché invece Main, l'accoppiamento sarà effettivamente piuttosto basso. Il codice chiamante non sa nemmeno che sta effettivamente chiamando l'ultimo metodo sull'oggetto originale, il che illustra in realtà il grado di disaccoppiamento.
Raggiungi un percorso di astrazioni concise e ortogonali, piuttosto che in profondità all'interno di una implementazione.


Punto molto interessante sul grande aumento del numero di parametri.
Oak,

2

La Legge di Demetra , come sottolinea @ Péter Török, suggerisce la forma "compatta".

Inoltre, maggiore è il numero di metodi menzionati esplicitamente nel codice, più classi dipendono dalla classe, aumentando i problemi di manutenzione. Nel tuo esempio, la forma compatta dipende da due classi, mentre la forma più lunga dipende da quattro classi. La forma più lunga non solo viola la Legge di Demetra; ti farà anche cambiare il tuo codice ogni volta che cambi uno dei quattro metodi di riferimento (al contrario di due nella forma compatta).


D'altra parte, seguire ciecamente quella legge significa che il numero di metodi Aesploderà con molti metodi che Apotrebbero voler delegare via comunque. Tuttavia, sono d'accordo con le dipendenze - ciò riduce drasticamente la quantità di dipendenze richieste dal codice client.
Oak,

1
@Oak: fare qualcosa alla cieca non è mai un bene. È necessario esaminare pro e contro e prendere decisioni basate su prove. Ciò include anche la Legge di Demetra.
CesarGon,

2

Ho affrontato questo problema da solo. Lo svantaggio di "raggiungere" in profondità diversi oggetti è che quando esegui il refactoring finirai per dover cambiare un sacco di codice poiché ci sono così tante dipendenze. Inoltre, il tuo codice diventa un po 'gonfio e più difficile da leggere.

D'altra parte, avere classi che semplicemente "passano" i metodi significa anche un sovraccarico di dover dichiarare un multiplo di metodi in più di un posto.

Una soluzione che mitiga questo ed è appropriato in alcuni casi è avere una classe factory che costruisce una sorta di oggetto di facciata copiando dati / oggetti dalle classi di apprendistato. In questo modo è possibile codificare l'oggetto della facciata e, quando si esegue il refactoring, si modifica semplicemente la logica della fabbrica.


1

Trovo spesso che la logica di un programma sia più facile da usare con metodi concatenati. Per me, si customer.getLastInvoice().itemCount()adatta al mio cervello meglio di customer.countLastInvoiceItems().

Se valga la pena il mal di testa di manutenzione di avere l'accoppiamento extra dipende da te. (Mi piacciono anche le piccole funzioni in classi piccole, quindi tendo a concatenarmi. Non sto dicendo che sia giusto - è proprio quello che faccio.)


che dovrebbe essere customer.NrLastInvoices o customer.LastInvoice.NrItems IMO. Quella catena non è troppo lunga, quindi probabilmente non vale la pena appiattirla se la quantità di combinazioni è piuttosto grande
Homde,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.