SOLIDI vs. metodi statici


11

Ecco un problema che mi capita spesso di incontrare: lasciare che ci sia un progetto di negozio web che abbia una classe di prodotto. Voglio aggiungere una funzione che consente agli utenti di pubblicare recensioni su un prodotto. Quindi ho una classe Review che fa riferimento a un prodotto. Ora ho bisogno di un metodo che elenca tutte le recensioni su un prodotto. Esistono due possibilità:

(UN)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Guardando il codice, sceglierei (A): non è statico e non ha bisogno di un parametro. Tuttavia, sento che (A) viola il principio di responsabilità singola (SRP) e il principio aperto-chiuso (OCP) mentre (B) non:

  • (SRP) Quando voglio cambiare il modo in cui le recensioni vengono raccolte per un prodotto, devo cambiare la classe del Prodotto. Ma dovrebbe esserci solo un motivo per cambiare la classe di prodotto. E non sono certo le recensioni. Se confeziono tutte le funzionalità che hanno qualcosa a che fare con i prodotti nel Prodotto, saranno presto distrutte.

  • (OCP) Devo cambiare la classe di prodotto per estenderla con questa funzione. Penso che ciò violi la parte del principio "Chiuso per il cambiamento". Prima di ottenere la richiesta del cliente per l'implementazione delle recensioni, ho considerato il Prodotto finito e lo ho "chiuso".

Cosa è più importante: seguire i principi SOLID o avere un'interfaccia più semplice?

O sto facendo qualcosa di sbagliato qui del tutto?

Risultato

Wow, grazie per tutte le tue fantastiche risposte! È difficile sceglierne uno come risposta ufficiale.

Consentitemi di riassumere gli argomenti principali dalle risposte:

  • pro (A): OCP non è una legge e anche la leggibilità del codice è importante.
  • pro (A): la relazione tra entità dovrebbe essere navigabile. Entrambe le classi potrebbero conoscere questa relazione.
  • pro (A) + (B): esegui entrambe le operazioni e delega in (A) a (B) in modo che il Prodotto abbia meno probabilità di essere nuovamente modificato.
  • pro (C): mette i metodi finder in terza classe (servizio) dove non è statico.
  • contra (B): impedisce di deridere nei test.

Alcune cose aggiuntive che i miei college al lavoro hanno contribuito:

  • pro (B): il nostro framework ORM può generare automaticamente il codice per (B).
  • pro (A): per motivi tecnici del nostro framework ORM, in alcuni casi sarà necessario modificare l'entità "chiusa", indipendentemente da dove si trova il cercatore. Quindi non sarò sempre in grado di attenermi a SOLID, comunque.
  • contra (C): troppe storie ;-)

Conclusione

Sto usando entrambi (A) + (B) con delega per il mio progetto attuale. In un ambiente orientato ai servizi, tuttavia, andrò con (C).


2
Finché non è una variabile statica, tutto va bene. Metodi statici semplici da testare e semplici da tracciare.
Coder

3
Perché non avere solo una classe ProductsReviews? Quindi il prodotto e la recensione rimangono gli stessi. O forse fraintendo.
ElGringoGrande,

2
@Coder "I metodi statici sono semplici da testare", davvero? Non possono essere derisi, vedi: googletesting.blogspot.com/2008/12/… per maggiori dettagli.
Stuper:

1
@StuperUser: non c'è nulla da deridere. Assert(5 = Math.Abs(-5));
Coder

2
Il test Abs()non è il problema, testare qualcosa che dipende da esso è. Non hai una cucitura per isolare il Code-Under-Test (CUT) dipendente per usare un finto. Ciò significa che non è possibile testarlo come unità atomica e tutti i test diventano test di integrazione che testano la logica dell'unità. Un fallimento in un test potrebbe essere in CUT o in Abs()(o nel suo codice dipendente) e rimuove i benefici della diagnosi dei test unitari.
Stuper:

Risposte:


7

Cosa è più importante: seguire i principi SOLID o avere un'interfaccia più semplice?

Interfaccia con SOLID

Questi non si escludono a vicenda. L'interfaccia dovrebbe esprimere la natura del tuo modello di business idealmente in termini di modello di business. I principi SOLID sono un Koan per massimizzare la manutenibilità del codice orientato agli oggetti (e intendo "manutenibilità" nel senso più ampio). Il primo supporta l'uso e la manipolazione del modello di business e il secondo ottimizza la manutenzione del codice.

Principio aperto / chiuso

"non toccarlo!" è un'interpretazione troppo semplicistica. E supponendo che intendiamo "la classe" è arbitraria e non necessariamente corretta. Piuttosto, OCP significa che hai progettato il tuo codice in modo tale da modificarlo (non dovrebbe) richiedere di modificare direttamente il codice funzionante esistente. Inoltre, non toccare il codice in primo luogo è il modo ideale per preservare l'integrità delle interfacce esistenti; questo è un corollario significativo dell'OCP secondo me.

Infine vedo OCP come un indicatore della qualità del design esistente. Se mi trovo a rompere classi aperte (o metodi) troppo spesso, e / o senza ragioni veramente solide (ah, ah) per farlo, questo potrebbe dirmi che ho un cattivo design (e / o no sapere come codificare OO).

Fai del tuo meglio, abbiamo una squadra di medici in attesa

Se l'analisi dei requisiti ti dice che devi esprimere la relazione Revisione Prodotto da entrambe le prospettive, allora fallo.

Pertanto, Wolfgang, potresti avere una buona ragione per modificare quelle classi esistenti. Dati i nuovi requisiti, se una Revisione è ora una parte fondamentale di un Prodotto, se ogni estensione di Prodotto ha bisogno di Revisione, in tal caso rende il codice client opportunamente espressivo, quindi integrarlo nel Prodotto.


1
+1 per notare che OCP è più un indicatore di buona qualità, non una regola veloce per qualsiasi codice. Se ritieni che sia difficile o impossibile seguire correttamente l'OCP, questo è un segno che il tuo modello deve essere sottoposto a refactoring per consentire un riutilizzo più flessibile.
CodexArcanum,

Ho scelto l'esempio di Prodotto e Revisione per sottolineare che Prodotto è un'entità centrale del progetto, mentre Revisione è un semplice componente aggiuntivo. Quindi il Prodotto è già esistente, finito e la Revisione arriva più tardi e dovrebbe essere introdotto senza aprire il codice esistente (incl. Prodotto).
Wolfgang,

10

I SOLIDI sono linee guida, quindi influenzano il processo decisionale piuttosto che dettarlo.

Una cosa da tenere presente quando si utilizzano metodi statici è il loro impatto sulla testabilità .


Il test forProduct(Product product)non sarà un problema.

Sarà testare qualcosa che dipende da esso.

Non avrai una cucitura per isolare il Code-Under-Test (CUT) dipendente per usare un mock, poiché quando l'applicazione è in esecuzione, i metodi statici necessariamente esistono.

Diamo un metodo chiamato CUT()che chiamaforProduct()

In caso contrario, non forProduct()è staticpossibile eseguire il test CUT()come unità atomica e tutti i test diventano test di integrazione che testano la logica dell'unità.

Un fallimento in un test per CUT, potrebbe essere causato da un problema in CUT()o in forProduct()(o in uno dei suoi codici dipendenti) che rimuove i benefici della diagnosi dei test unitari.

Vedi questo eccellente post sul blog per informazioni più dettagliate: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Ciò può portare alla frustrazione con il fallimento dei test e l'abbandono delle buone pratiche e dei benefici che le circondano.


1
Questo è un ottimo punto. In generale, non per me però. ;-) Non sono così severo nello scrivere test unit che coprono più di una classe. Scendono tutti nel database, va bene per me. Quindi non deriderò l'oggetto business o il suo finder.
Wolfgang,

+1, grazie per aver impostato la bandiera rossa per i test! Sono d'accordo con @Wolfgang, di solito non sono così severo ma quando ho bisogno di quel tipo di test odio davvero i metodi statici. Di solito se un metodo statico interagisce troppo con i suoi parametri o se interagisce con qualsiasi altro metodo statico, preferisco renderlo un metodo di istanza.
Adriano Repetti,

Questo non dipende interamente dalla lingua utilizzata? L'OP ha usato un esempio Java, ma non ha mai menzionato una lingua nella domanda, né quella specificata nei tag.
Izkata,

1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Credo che Javascript e Python consentano entrambi di ignorare / deridere i metodi statici. Non sono sicuro al 100%, però.
Izkata,

1
@Izkata JS è digitato in modo dinamico, quindi non è così static, lo simuli con una chiusura e il modello singleton. Leggendo su Python (in particolare stackoverflow.com/questions/893015/… ), devi ereditare ed estendere. L'override non è beffardo; sembra che tu non abbia ancora una cucitura per testare il codice come unità atomica.
Stuper:

4

Se ritieni che il Prodotto sia il posto giusto per trovare le Recensioni del prodotto, puoi sempre offrire al Prodotto una classe di aiuto per svolgere il proprio lavoro. (Puoi dirlo perché la tua attività non parlerà mai di una recensione se non in termini di prodotto).

Ad esempio, sarei tentato di iniettare qualcosa che ha svolto il ruolo di revisore delle recensioni. Probabilmente gli darei l'interfaccia IRetrieveReviews. Puoi metterlo nel costruttore del prodotto (Iniezione delle dipendenze). Se si desidera cambiare il modo le recensioni vengono recuperati si può fare facilmente iniettando un collaboratore diverso - una TwitterReviewRetrievero un AmazonReviewRetrievero di MultipleSourceReviewRetrievero qualsiasi altra cosa che vi serve.

Entrambi ora hanno una sola responsabilità (essendo il punto di riferimento per tutte le cose relative al prodotto e recuperando le recensioni, rispettivamente), e in futuro il comportamento del prodotto rispetto alle recensioni può essere modificato senza cambiare effettivamente il prodotto (è possibile estenderlo come ProductWithReviewsse volessi davvero essere pedante sui tuoi principi SOLIDI, ma questo sarebbe abbastanza buono per me).


Sembra il modello DAO che è molto comune nel software orientato al servizio / ai componenti. Mi piace questa idea perché esprime che il recupero di oggetti non è responsabilità di questi oggetti. Tuttavia, poiché preferisco un orientamento orientato agli oggetti rispetto al servizio orientato.
Wolfgang,

1
L'uso di un'interfaccia come IRetrieveReviewssmette di essere orientato al servizio - non determina cosa ottiene le recensioni, o come o quando. Forse è un servizio con molti metodi per cose come questa. Forse è una classe che fa quell'unica cosa. Forse è un repository o fa una richiesta HTTP a un server. Non lo sai. Non dovresti saperlo. Questo è il punto.
Lunivore,

Sì, quindi sarebbe un'implementazione del modello di strategia. Quale sarebbe un argomento per una terza classe. (A) e (B) non lo supporterebbero. Il finder utilizzerà sicuramente un ORM, quindi non c'è motivo di sostituire l'algoritmo. Scusami se non ero chiaro su questo nella mia domanda.
Wolfgang

3

Avrei una classe ProductsReview. Dici che la recensione è nuova comunque. Ciò non significa che possa essere qualsiasi cosa. Deve ancora avere solo una ragione per cambiare. Se cambi il modo in cui ottieni le recensioni per qualsiasi motivo, dovresti cambiare la classe Review.

Non è corretto

Stai mettendo il metodo statico nella classe Review perché ... perché? Non è quello con cui stai lottando? Non è l'intero problema?

Allora no. Crea una classe a cui l'unica responsabilità è ottenere le recensioni dei prodotti. È quindi possibile sottoclassarlo in ProductReviewsByStartRating qualunque. O esegui la sottoclasse per ottenere recensioni per una classe di prodotti.


Non sono d'accordo sul fatto che avere il metodo su Review violerebbe SRP. Sul prodotto, sarebbe ma non sulla revisione. Il mio problema era che era statico. Se avessi spostato il metodo su una terza classe, sarebbe comunque statico e con il parametro del prodotto.
Wolfgang,

Quindi stai dicendo che l'unica responsabilità per la classe Review, l'unica ragione per cui cambia, è se il metodo statico forProduct deve cambiare? Non ci sono altre funzionalità nella classe Review?
ElGringoGrande,

Ci sono molte cose su Review. Ma penso che un forProduct (prodotto) si adatterebbe perfettamente ad esso. La ricerca delle recensioni dipende molto dagli attributi e dalla struttura della recensione (quali identificatori univoci, quali ambiti che cambiano gli attributi?). Il prodotto, tuttavia, non è a conoscenza e non dovrebbe essere a conoscenza delle recensioni.
Wolfgang,

3

Non inserirò la funzionalità "Ottieni recensioni per prodotto" né nella Productclasse né nella Reviewclasse ...

Hai un posto dove recuperare i tuoi prodotti, giusto? Qualcosa con GetProductById(int productId)e forse GetProductsByCategory(int categoryId)e così via.

Allo stesso modo, dovresti avere un posto dove recuperare le tue recensioni, con un GetReviewbyId(int reviewId)e forse un GetReviewsForProduct(int productId).

Quando voglio cambiare il modo in cui le recensioni vengono raccolte per un prodotto, devo cambiare la classe del Prodotto.

Se si separa l'accesso ai dati dalle classi di dominio, non sarà necessario modificare nessuna delle due classi di dominio quando si cambia il modo in cui vengono raccolte le recensioni.


Questo è come lo gestirò, e sono confuso (e preoccupato se le mie idee sono appropriate) dalla mancanza di questa risposta fino al tuo post. Chiaramente né la classe che rappresenta un rapporto né la rappresentazione di un prodotto dovrebbero essere responsabili del recupero effettivo dell'altro. Alcuni tipi di dati che forniscono servizi dovrebbero gestirli.
CodexArcanum,

@CodexArcanum Beh, non sei solo. :-)
Eric King

2

Modelli e principi sono linee guida, non regole scritte nella pietra. A mio avviso, la domanda non è se è meglio seguire i principi SOLID o mantenere un'interfaccia più semplice. Ciò che dovresti chiederti è ciò che è più leggibile e comprensibile per la maggior parte delle persone. Spesso questo significa che deve essere il più vicino possibile al dominio.

In questo caso preferirei la soluzione (B) perché per me il punto di partenza è il Prodotto, non la Recensione ma immagino che stai scrivendo un software per gestire le recensioni. In quel caso il centro è la Revisione, quindi la soluzione (A) potrebbe essere preferibile.

Quando ho molti metodi come questo ("connessioni" tra le classi) li spoglio tutti fuori e creo una (o più) nuove classi statiche per organizzarle. Di solito puoi vederli come query o tipo di repository.


Stavo anche pensando a questa idea della semantica di entrambe le estremità e di quale sia "più forte" in modo da rivendicare il cercatore. Ecco perché mi è venuta in mente (B). Aiuterebbe anche a creare una gerarchia di "astrazione" delle classi aziendali. Il prodotto era un mero oggetto di base in cui Review è superiore. Pertanto, Review può fare riferimento al Prodotto ma non viceversa. In questo modo, è possibile evitare cicli nei riferimenti tra le business class, il che costituirebbe un altro problema sulla mia lista risolto.
Wolfgang,

Penso che per un piccolo insieme di classi potrebbe essere bello anche fornire ENTRAMBE i metodi (A) e (B). Troppe volte l'interfaccia utente non è ben nota al momento della stesura di questa logica.
Adriano Repetti,

1

Il tuo prodotto può semplicemente delegare al tuo metodo di revisione statico, nel qual caso stai fornendo una comoda interfaccia in una posizione naturale (Product.getReviews) ma i dettagli di implementazione sono in Review.getForProduct.

SOLID sono linee guida e dovrebbero risultare in un'interfaccia semplice e sensibile. In alternativa, è possibile derivare SOLID da interfacce semplici e sensibili. Riguarda la gestione delle dipendenze all'interno del codice. L'obiettivo è ridurre al minimo le dipendenze che creano attrito e creano barriere all'inevitabile cambiamento.


Non sono sicuro che lo vorrei. In questo modo, ho il metodo in entrambi i posti, il che è positivo perché altri sviluppatori non hanno bisogno di guardare in entrambe le classi per vedere dove l'ho messo. D'altra parte, avrei entrambi gli svantaggi del metodo statico e la modifica della classe di prodotto "chiusa". Non sono sicuro che la delegazione aiuti a risolvere il problema dell'attrito. Se c'è mai stato un motivo per cambiare il cercatore, sicuramente si tradurrà nel cambio della firma e quindi di nuovo nel Prodotto.
Wolfgang,

La chiave di OCP è che è possibile sostituire le implementazioni senza modificare il codice. In pratica, questo è strettamente legato alla sostituzione di Liskov. Un'implementazione deve essere sostituibile con un'altra. Potresti avere DatabaseReviews e SoapReviews come classi diverse sia implementando IGetReviews.getReviews sia getReviewsForProducts. Quindi il tuo sistema è Apri per estensione (cambiando il modo in cui vengono ottenute le recensioni) e Chiuso per modifica (le dipendenze su IGetReviews non si rompono). Questo è ciò che intendo per gestione delle dipendenze. Nel tuo caso, dovrai modificare il codice per cambiarne il comportamento.
Pfries

Non è il DIP (Dependency Inversion Principle) che stai descrivendo?
Wolfgang,

Sono principi correlati. Il tuo punto di sostituzione è raggiunto da DIP.
Pfries

1

Ho un'idea leggermente diversa rispetto alla maggior parte delle altre risposte. Penso che Productsia Reviewfondamentalmente un oggetto per il trasferimento di dati (DTO). Nel mio codice cerco di impedire ai miei DTO / Entità di avere comportamenti. Sono solo una bella API per memorizzare lo stato attuale del mio modello.

Quando parli di OO e SOLID, in genere parli di un "oggetto" che non rappresenta lo stato (necessariamente), ma invece rappresenta un tipo di servizio che risponde a domande per te o al quale puoi delegare parte del tuo lavoro . Per esempio:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Quindi il tuo effettivo ProductRepositoryrestituirà un ExistingProductper il GetProductByProductIdmetodo, ecc.

Ora stai seguendo il principio della singola responsabilità (qualsiasi cosa erediti da IProductè solo trattenersi per affermare, e qualsiasi cosa erediti da IProductRepositoryè responsabile per sapere come persistere e reidratare il tuo modello di dati).

Se si modifica lo schema del database, è possibile modificare l'implementazione del repository senza modificare i DTO, ecc.

Quindi, insomma, non sceglierei nessuna delle tue opzioni. :)


0

I metodi statici possono essere più difficili da testare, ma ciò non significa che non puoi usarli - devi solo configurarli in modo da non doverli testare.

Crea sia product.GetReviews che Review.ForProduct metodi a una riga che sono qualcosa di simile

new ReviewService().GetReviews(productID);

ReviewService contiene tutto il codice più complesso e ha un'interfaccia progettata per la testabilità, ma non è esposta direttamente all'utente.

Se devi avere una copertura del 100%, chiedi ai tuoi test di integrazione di chiamare i metodi della classe prodotto / recensione.

Potrebbe essere utile se si pensa alla progettazione dell'API pubblica piuttosto che alla progettazione della classe - in quel contesto è importante una semplice interfaccia con il raggruppamento logico - la struttura del codice reale conta solo per gli sviluppatori.

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.