La "composizione sull'ereditarietà" viola il "principio secco"?

36

Ad esempio, considera di avere una classe per altre classi da estendere:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

e alcune sottoclassi:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

In effetti, la sottoclasse non ha alcun metodo per sovrascrivere, inoltre non accedo alla HomePage in modo generico, cioè: Non vorrei fare qualcosa del tipo:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Voglio solo riutilizzare la pagina di accesso. Ma secondo il link , preferisco la composizione qui perché non ho bisogno dell'interfaccia LoginPage, quindi non uso l'ereditarietà qui :

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

ma il problema arriva qui, nella nuova versione, il codice:

public LoginPage loginPage;

duplicati quando viene aggiunta una nuova classe. E se LoginPage ha bisogno di setter e getter, devono essere copiati più codici:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Quindi la mia domanda è, è "la composizione sull'ereditarietà" che viola il "principio secco"?

    
posta mmmaaa 12.02.2018 - 04:04
fonte

3 risposte

46

Aspetti che ti preoccupi di ripetere

public LoginPage loginPage;

in due posti viola DRY? Da quella logica

int x;

ora può esistere solo sempre in un oggetto nell'intero codice base. Bleh.

DRY è una buona cosa da tenere a mente ma dai. Inoltre

... extends LoginPage

è duplicato nella tua alternativa quindi anche il fatto di essere anale su DRY non darà un senso a questo.

Le valute di DRY valide tendono a concentrarsi su comportamenti identici necessari in più punti definiti in più punti, in modo tale che la necessità di modificare questo comportamento richieda una modifica in più punti. Prendi le decisioni in un posto e dovrai solo cambiarle in un unico posto. Ciò non significa che solo un oggetto possa mai contenere un riferimento alla tua LoginPage.

DRY non dovrebbe essere seguito alla cieca. Se stai duplicando perché copiare e incollare è più facile che pensare un buon metodo o nome di classe, allora probabilmente hai torto.

Ma se vuoi mettere lo stesso codice in un posto diverso perché quel posto diverso è soggetto a una diversa responsabilità e probabilmente avrà bisogno di cambiare in modo indipendente allora è probabilmente saggio rilassare l'applicazione di DRY e lasciare che questo comportamento identico abbia una diversa identità. È lo stesso tipo di pensiero che va nel proibire i numeri magici.

DRY non riguarda solo l'aspetto del codice. Si tratta di non diffondere i dettagli di un'idea in giro con ripetizioni insensate costringendo i manutentori a sistemare le cose ricorrendo a ripetizioni insensate. È quando provi a dire a te stesso che la ripetizione insensata è solo la tua convenzione che le cose si stanno dirigendo in un modo davvero brutto.

Quello che penso tu stia davvero cercando di lamentarti è chiamato codice boilerplate. Sì, l'uso della composizione piuttosto che l'ereditarietà richiede il codice boilerplate. Niente viene esposto gratuitamente, devi scrivere il codice che lo espone. Con questo boilerplate arriva la flessibilità dello stato, la capacità di restringere l'interfaccia esposta, di dare nomi diversi che siano appropriati per il loro livello di astrazione, buona o indiretta, e tu stai usando ciò che sei composto dall'esterno, non il all'interno, quindi stai affrontando la normale interfaccia.

Ma sì, è un sacco di digitazione della tastiera in più. Finché posso impedire il problema yo-yo di rimbalzare su e giù uno stack ereditario mentre leggo il codice ne vale la pena.

Ora non è che mi rifiuto di usare l'ereditarietà. Uno dei miei usi preferiti è dare nuovi nomi di eccezioni:

public class MyLoginPageWasNull extends NullPointerException{}
    
risposta data 12.02.2018 - 07:04
fonte
128

Un comune fraintendimento con il principio DRY è che è in qualche modo correlato a non ripetere linee di codice. Il principio DRY è "Ogni elemento di conoscenza deve avere una rappresentazione unica, non ambigua e autorevole all'interno di un sistema" . Riguarda la conoscenza, non il codice.

LoginPage sa come disegnare la pagina per l'accesso. Se EditInfoPage sapeva come fare, allora quella sarebbe una violazione. Includere LoginPage tramite composizione è non in alcun modo una violazione del principio DRY.

Il principio DRY è forse il principio più abusato dell'ingegneria del software e dovrebbe sempre essere considerato non come un principio per non duplicare il codice, ma come principio per non duplicare la conoscenza del dominio astratto. In realtà, in molti casi se applichi correttamente DRY, allora duplicherà il codice e questo non è necessariamente un aspetto negativo.

    
risposta data 12.02.2018 - 08:22
fonte
12

Risposta breve: sì, lo fa - ad un grado minore, accettabile.

A prima vista, l'ereditarietà a volte può farti risparmiare alcune righe di codice, poiché ha l'effetto di dire "la mia classe di riutilizzo conterrà tutti i metodi e gli attributi pubblici in un modo 1: 1". Quindi se c'è un elenco di 10 metodi in un componente, non è necessario ripeterli nel codice nella classe ereditata. Quando in uno scenario di composizione 9 di questi 10 metodi devono essere esposti pubblicamente attraverso un componente riusatore, allora si devono scrivere 9 chiamate delegate e lasciare il rimanente fuori, non c'è modo di aggirare questo.

Perché è tollerabile? Guarda i metodi che sono replicati in uno scenario di composizione: questi metodi sono esclusivamente che delegano le chiamate all'interfaccia del componente, quindi non contengono alcuna logica reale.

Il cuore del principio DRY è di evitare di avere due punti nel codice in cui sono codificate le stesse regole logiche - perché quando cambiano queste regole logiche, in un codice non DRY è facile adattare uno di quei posti e dimentica l'altro, che introduce un errore.

Ma dal momento che la delega delle chiamate non contiene la logica, queste non sono normalmente soggette a tale cambiamento, quindi non causano un vero problema quando "preferisco la composizione sull'ereditarietà". E anche se l'interfaccia del componente cambia, il che potrebbe indurre modifiche formali a tutte le classi usando il componente, in un linguaggio compilato il compilatore ci dirà quando ci siamo dimenticati di cambiare uno dei chiamanti.

Una nota al tuo esempio: non so come assomigliano il HomePage e il EditInfoPage , ma se hanno funzionalità di accesso e un HomePage (o un EditInfoPage ) è un LoginPage , quindi l'ereditarietà potrebbe essere lo strumento corretto qui. Un esempio meno discutibile, in cui la composizione sarà lo strumento migliore in un modo più ovvio, probabilmente renderebbe le cose più chiare.

Supponendo che né HomePageEditInfoPage siano LoginPage , e uno vuole riutilizzare quest'ultimo, come hai scritto, piuttosto che è probabile che uno richieda solo alcune parti del LoginPage , non di tutto. In tal caso, un approccio migliore rispetto all'uso della composizione nel modo illustrato potrebbe essere

  • estrai la parte riutilizzabile di LoginPage in un componente a sé stante

  • riutilizza quel componente in HomePage e EditInfoPage nello stesso modo in cui è ora utilizzato in LoginPage

In questo modo, in genere diventerà molto più chiaro perché e quando la composizione sull'ereditarietà è l'approccio giusto.

    
risposta data 12.02.2018 - 07:30
fonte

Leggi altre domande sui tag