La "composizione per eredità" viola il "principio secco"?


36

Ad esempio, considera che ho 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 accederei alla HomePage in modo generico, cioè: non farei qualcosa del tipo:

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

Voglio solo riutilizzare la pagina di accesso. Ma secondo https://stackoverflow.com/a/53354 , dovrei preferire 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 necessita di setter e getter, è necessario copiare 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'eredità" viola il "principio secco"?


13
A volte non hai bisogno né di eredità né di composizione, vale a dire che a volte una classe con pochi oggetti fa il lavoro.
Erik Eidt,

23
Perché vorresti che tutte le tue pagine fossero la pagina di accesso? Forse hai solo una pagina di accesso.
Sig. Cochese,

29
Ma, con l'eredità, hai duplicato extends LoginPagedappertutto. CHECK MATE!
el.pescado,

2
Se riscontri molto il problema nell'ultimo frammento, potresti usare eccessivamente getter e setter. Tendono a violare l'incapsulamento.
Brian McCutchon,

4
Quindi, fai in modo che la tua pagina possa essere decorata e rendi il tuo LoginPagedecoratore. Niente più duplicati, solo un semplice page = new LoginPage(new EditInfoPage())e il gioco è fatto. Oppure si utilizza il principio aperto-chiuso per creare il modulo di autenticazione che può essere aggiunto dinamicamente a qualsiasi pagina. Esistono molti modi per gestire la duplicazione del codice, principalmente attraverso la ricerca di una nuova astrazione. LoginPageè probabilmente un brutto nome, ciò che si desidera assicurarsi è che l'utente sia autenticato durante la navigazione di quella pagina, mentre viene reindirizzato al LoginPageo mostrato un messaggio di errore corretto se non lo è.
Polygnome,

Risposte:


46

Aspetta che ti preoccupi di ripetere

public LoginPage loginPage;

in due punti viola SECCO? Secondo quella logica

int x;

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

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

... extends LoginPage

viene duplicato nella tua alternativa, quindi anche essere anale su DRY non ha senso.

Le preoccupazioni in merito ai SECCHI SECCHI tendono a concentrarsi su comportamenti identici necessari in più punti definiti in più luoghi in modo tale che la necessità di cambiare questo comportamento finisca per richiedere un cambiamento in più punti. Prendi le decisioni in un unico posto e dovrai solo cambiarle in un unico posto. Ciò non significa che solo un oggetto possa mai contenere un riferimento a LoginPage.

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

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

DRY non riguarda solo l'aspetto del codice. Si tratta di non diffondere i dettagli di un'idea con ripetizione insensata costringendo i manutentori a sistemare le cose usando la ripetizione insensata. È quando provi a dire a te stesso che la ripetizione insensata è solo la tua convenzione che le cose stanno andando in una brutta maniera.

Quello che penso tu stia davvero provando a lamentarti si chiama codice boilerplate. Sì, l'uso della composizione piuttosto che dell'ereditarietà richiede il codice boilerplate. Niente viene esposto gratuitamente, devi scrivere il codice che lo espone. Con quella caldaia deriva la flessibilità dello stato, la capacità di restringere l'interfaccia esposta, di dare alle cose nomi diversi che sono appropriati per il loro livello di astrazione, buona ol indiretta, e stai usando ciò di cui sei composto dall'esterno, non il all'interno, quindi stai affrontando la sua normale interfaccia.

Ma sì, è un sacco di extra digitando la tastiera. Finché posso prevenire il problema yo-yo di far rimbalzare su e giù uno stack di ereditarietà mentre leggo il codice, ne vale la pena.

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

public class MyLoginPageWasNull extends NullPointerException{}

int x;può esistere non più di zero volte in una base di codice
K. Alan Bates,

@ K.AlanBates mi scusi ?
candied_orange,

... sapevo che avresti replicato con la classe Point lol. A nessuno importa della classe Point. Se entro nella tua base di codice e vedo int a; int b; int x;che spingerò per rimuovere "tu".
K. Alan Bates,

@ K.AlanBates Wow. è triste che tu pensi che quel tipo di comportamento ti renderebbe un buon programmatore. Il miglior programmatore non è quello che può trovare più cose sugli altri di cui lamentarsi. È quello che rende il resto migliore.
candied_orange,

L'uso di nomi insignificanti non è una novità e probabilmente rende lo sviluppatore di quel codice una responsabilità piuttosto che una risorsa.
K. Alan Bates,

127

Un malinteso comune con il principio DRY è che è in qualche modo correlato al non ripetere righe di codice. Il principio DRY è "Ogni pezzo di conoscenza deve avere un'unica, inequivocabile, autorevole rappresentazione all'interno di un sistema" . Riguarda la conoscenza, non il codice.

LoginPagesa come disegnare la pagina per il login. Se EditInfoPagesapessi come farlo, sarebbe una violazione. Includere LoginPagetramite composizione non è in alcun modo una violazione del principio DRY.

Il principio DRY è forse il principio più abusato nell'ingegneria del software e dovrebbe sempre essere considerato non come un principio per non duplicare il codice, ma come un principio per non duplicare la conoscenza del dominio astratto. In realtà, in molti casi se si applica DRY correttamente, si duplicherà il codice e questa non è necessariamente una cosa negativa.


27
Il "principio della responsabilità singola" viene utilizzato molto di più. "Non ripeterti" potrebbe facilmente entrare come numero due :-)
gnasher729,

28
"DRY" è un utile acronimo di "Don't Repeat Yourself", che è una frase acchiappata, ma non il nome del principio, il nome effettivo del principio è "Once And Only Once", che a sua volta è un nome accattivante per un principio che richiede un paio di paragrafi per descrivere. L'importante è capire la coppia di paragrafi, non memorizzare le tre lettere D, R e Y.
Jörg W Mittag

4
@ gnasher729 Sai, in realtà sono d'accordo con te sul fatto che SRP è probabilmente usato molto di più. Anche se per essere onesti, in molti casi penso che siano spesso usati male insieme. La mia teoria è che i programmatori in generale non dovrebbero avere fiducia nel maneggiare acronimi con "nomi facili".
wasatz,

17
IMHO questa è una bella risposta. Tuttavia, per essere onesti: secondo la mia esperienza, la forma più frequente di violazioni DRY è causata dalla programmazione di copia-incolla e dalla duplicazione del codice. Qualcuno mi ha detto una volta "ogni volta che hai intenzione di copiare e incollare un po 'di codice, pensa due volte se la parte duplicata non può essere estratta in una funzione comune" - che è stato un ottimo consiglio IMHO.
Doc Brown,

9
L'abuso di copia-incolla è certamente una forza trainante per la violazione di DRY, ma semplicemente vietare la copia-incolla è una cattiva guida per seguire DRY. Non proverei alcun rimorso per avere due metodi con codice identico quando quei metodi rappresentano differenti conoscenze. Servono due diverse responsabilità. Oggi capita di avere un codice identico. Dovrebbero essere liberi di cambiare in modo indipendente. Sì, conoscenza, non codice. Ben messo. Ho scritto una delle altre risposte ma mi inchinerò a questa. +1.
candied_orange,

12

Risposta breve: sì, lo fa - in misura minore, accettabile.

A prima vista, l'ereditarietà a volte può salvarti 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 esiste 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 di riutilizzo, si deve scrivere 9 chiamate deleganti e lasciare uscire quella rimanente, non c'è modo di aggirare questo.

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

Il cuore del principio DRY è quello di evitare di avere due posizioni nel codice in cui sono codificate le stesse regole logiche . Perché quando queste regole logiche cambiano, nel codice non DRY è facile adattare uno di quei luoghi e dimenticare l'altro, che introduce un errore.

Ma poiché delegare le chiamate non contiene logica, queste non sono normalmente soggette a tale cambiamento, quindi non causano un vero problema quando "preferiscono la composizione rispetto all'eredità". E anche se l'interfaccia del componente cambia, il che potrebbe indurre cambiamenti formali in 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 tu HomePagee il tuoEditInfoPage , ma se hanno la funzionalità di accesso e un HomePage(o un EditInfoPage) è un LoginPage , allora 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 HomePageEditInfoPage sono una LoginPage, e vuole riutilizzare questi ultimi, come hai scritto, che è abbastanza probabile che uno richiede solo alcune parti del LoginPage, non tutto. In tal caso, potrebbe essere un approccio migliore rispetto all'uso della composizione nel modo mostrato

  • estrarre la parte riutilizzabile LoginPagein un componente a sé stante

  • riutilizzare quel componente in HomePage e EditInfoPagenello stesso modo in cui viene utilizzato ora all'internoLoginPage

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

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.