Metodo di concatenamento vs incapsulamento

17

Esiste il classico problema di OOP dei metodi di concatenamento di metodi rispetto a "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 A, B o C per esporlo.

Il lato negativo, 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 che ovviamente dipende dal contesto, ma mi piacerebbe davvero sentire qualche input su altre importanti differenze tra i due stili, e in quali circostanze dovrei preferire uno di essi - perché adesso, quando provo a progettare un codice, mi sembra di non utilizzare gli argomenti per decidere in un modo o nell'altro.

    
posta Oak 16.02.2011 - 10:24
fonte

6 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 singolo caso.

The advantage of following the Law of Demeter is that the resulting software tends to be more maintainable and adaptable. Since objects are less dependent on the internal structure of other objects, object containers can be changed without reworking their callers.

A disadvantage of the Law of Demeter is that it sometimes requires writing a large number of small "wrapper" methods to propagate method calls to the components. Furthermore, a class's interface can become bulky as it hosts methods for contained classes, resulting in a class without a cohesive interface. But this might also be a sign of bad OO design.

    
risposta data 16.02.2011 - 10:43
fonte
10

In generale, cerco di mantenere il metodo di concatenazione il più limitato possibile (basato sulla Legge di Demeter )

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

Martin Fowler fa la stessa distinzione in Lingue specifiche del dominio ma per ragioni di violazione comando di separazione delle query che afferma:

that every method should either be a command that performs an action, or a query that returns data to the caller, but not both.

Fowler nel suo libro a pagina 70 dice:

Command-query separation is an extremely valuable principle in programming, and I strongly encourage teams to use it. One of the consequences of using Method Chaining in internal DSLs is that it usually breaks this principle - each method alters state but returns an object to continue the chain. I have used many decibels disparaging people who don't follow command-query separation, and will do so again. But fluent interfaces follow a different set of rules, so I am happy to allow it here.

    
risposta data 16.02.2011 - 10:56
fonte
3

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

Nel primo caso, abbiamo

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

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

Dove main è di tipo IHasGetA . La domanda è: l'astrazione è idonea. La risposta non è banale. E in questo caso sembra un po 'fuori, 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é nel secondo esempio sia this che main dipendono dai tipi di v , w , x , y e z . Inoltre, il codice non sembra molto meglio, se ogni dichiarazione di metodo contiene una mezza dozzina di argomenti.

Un localizzatore di servizi richiede in realtà il primo approccio. Non vuoi accedere all'istanza creata tramite il localizzatore di servizio.

Quindi "raggiungere attraverso" un oggetto può creare molta dipendenza, tanto più se si basa sulle proprietà delle classi attuali.
Tuttavia creare un'astrazione significa 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!
    }
}

Dove main è un'istanza di Main . Se la classe conoscendo main riduce la dipendenza a IHasGetA invece di Main , troverai che l'accoppiamento è effettivamente piuttosto basso. Il codice chiamante non sa nemmeno che sta effettivamente chiamando l'ultimo metodo sull'oggetto originale, che in realtà illustra il grado di disaccoppiamento.
Raggiungi lungo un percorso di astrazioni concise e ortogonali, piuttosto che in profondità all'interno di implementazioni.

    
risposta data 16.02.2011 - 21:21
fonte
2

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

Inoltre, più metodi menzioni esplicitamente nel tuo codice, più classi dipendono dalla tua 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 contravviene alla Legge di Demetra; inoltre ti farà cambiare il tuo codice ogni volta che cambierai uno dei quattro metodi di riferimento (a differenza di due nel modulo compatto).

    
risposta data 16.02.2011 - 10:56
fonte
2

Ho lottato personalmente con questo problema. Il rovescio della medaglia con "raggiungere" in profondità oggetti diversi è che quando si esegue il refactoring si finisce per dover cambiare un sacco di codice poiché ci sono così tante dipendenze. Inoltre, il tuo codice diventa un po 'gonfio e difficile da leggere.

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

Una soluzione che lo attenua ed è appropriato in alcuni casi sta avendo una classe factory che costruisce una facciata oggetto di sorta copiando dati / oggetti dalle classi approriate. In questo modo puoi codificare il tuo oggetto facciata e quando ti rifatti devi semplicemente cambiare la logica della fabbrica.

    
risposta data 16.02.2011 - 19:50
fonte
1

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

Che valga la pena di manutenzione di avere l'accoppiamento extra dipende da te. (Mi piacciono anche le piccole funzioni in piccole classi, quindi tendo ad incatenare. Non sto dicendo che sia giusto, è solo quello che faccio.)

    
risposta data 16.02.2011 - 21:29
fonte

Leggi altre domande sui tag