È appropriato non seguire il principio O / C se si dispone della copertura del test unitario?

5

Trovo più comodo "modificare" le classi esistenti anziché estenderle. Pertanto, violento il principio di "principio chiuso e aperto" di "non modificare il codice sorgente compilato e testato, estendendo invece la funzionalità". Mi sento a mio agio nel modificare il codice sorgente a causa dei casi di test unitario. Ogni volta che sto modificando il codice, sono fiducioso che i test unitari mi terranno al corrente.

La mia domanda è, è opportuno modificare il codice sorgente abbastanza spesso se si hanno casi di test unitari ben definiti. O è meglio estendere le classi senza disturbare il codice originale?

    
posta Anmol Gupta 20.02.2016 - 10:22
fonte

7 risposte

13

Non esiste una "legge" che ti dice che tutto il tuo codice deve seguire l'OCP. Non è nemmeno considerata la "migliore pratica" per applicare questo a tutto il tuo codice. Questo è un principio applicabile solo se creerai librerie o moduli riutilizzabili in black box o un'architettura di plug-in (consiglio vivamente questo articolo di Bob Martin, chiama i sistemi di plugin "l'apoteosi" dell'OCP).

Meno è necessario modificare una libreria esistente per riutilizzarla in una nuova posizione, minori sono le probabilità che qualcosa si interromperà in un altro codice che dipende da quella libreria. Se la lib non deve essere modificata, non è possibile introdurre nuovi bug - questo non è un test unitario in grado di garantire. I test possono ridurre il rischio solo per questo, ma non possono mai provare l'assenza di bug.

Tuttavia, fare in modo che il codice della libreria confermi l'OCP richiede un certo sforzo, quindi è sempre un compromesso. Quando si tenta di creare un software di questo tipo riutilizzabile, in genere è necessario evolverlo in diverse iterazioni (supportate dai test delle unità) finché non raggiunge lo stato in cui non è necessario modificare ulteriormente il codice e può aggiungere nuovi requisiti (di un certo scopo) solo "per estensione". Per la mia esperienza, è necessario almeno 3 o 4 scenari reali di riutilizzo, confrontarli e pensare ai punti di estensione corretti per la lib per renderlo una "scatola nera". Altrimenti la libreria non raggiungerà mai quello stato (YMMV). Vedi questa precedente domanda & rispondi per un esempio dettagliato di come potrebbero apparire queste iterazioni.

Quindi, in breve, la domanda non dovrebbe essere "test OCP vs. unità". Utilizzare i test unitari per mantenere il codice in evoluzione e modificarlo per nuovi requisiti fino a raggiungere una qualità in cui non è necessario modificarlo ulteriormente per riutilizzarlo ed estenderlo.

    
risposta data 20.02.2016 - 14:19
fonte
3

Ci sono due domande lì:

  • Quanto spesso deve essere seguito OCP.
  • Quante volte viene seguito OCP.

Il primo può essere risposto obiettivamente:

Il cambiamento è un rischio nello sviluppo del software. Se sai qualcosa sulla gestione del rischio, sai che ogni rischio ha due componenti: una probabilità e un costo. Come sviluppatore di software, dovresti cercare di identificare i possibili rischi, le loro probabilità e i loro costi. Ora, cambiare il codice esistente è costoso. Questo perché se il codice cambia, allora tutti gli altri codici che dipendono dal codice modificato devono essere ricompilati, testati e riconvalidati. Per ridurre tale costo, puoi rendere il sistema o il modulo aperto a tale cambiamento. In questo modo, puoi semplicemente aggiungere nuovo codice e il vecchio codice rimane invariato, quindi a costo solo di test e convalida del nuovo codice. Ma c'è un altro costo: è che rendere il codice aperto a un cambiamento aumenta necessariamente la sua complessità e rendere il codice aperto a un cambiamento potrebbe rendere più difficile renderlo aperto a diversi cambiamenti.

In quanto tale, è lo sviluppatore a decidere quali modifiche presentano un alto rischio di occorrenza e a rendere strategicamente aperto il codice a tali modifiche. Nel tuo caso, puoi dire che stai pagando il costo di cambiare il codice esistente quando si verificano cambiamenti specifici. E questa non è una brutta cosa Ma se vedi che stai facendo un tipo di cambiamento più e più volte, potrebbe essere una buona idea rendere il codice aperto a tale cambiamento.

Ora, per la seconda domanda, questo verrà valutato:

Da tutto il codice che ho visto, direi che gli sviluppatori non rendono il codice sufficientemente aperto. Ci sono molti cambiamenti che sono abbastanza comuni in molti progetti e domini. Identificare quei tipi di modifiche e adattare il design a loro è fondamentale per mantenere il codice gestibile. Tuttavia, troppo spesso vedo zero OCP anche nei casi in cui i cambiamenti comuni sono chiari in vista.

    
risposta data 20.02.2016 - 12:18
fonte
2

Questa è una didascalia per la risposta di Lie Ryan.

Come quasi tutto nella programmazione, anche qui l'approccio non sarà simile per casi diversi.

Stai giustificando la procedura di modifica con test unitari ben scritti, ma anche in questo caso, modificare direttamente una classe o renderla aperta per estensione dipende solitamente da cosa stai programmando esattamente.

Caso I.

Quando ha senso modificare una classe esistente

Immagina di avere il tuo dominio contenente la logica aziendale della tua applicazione. In un mondo ideale, le regole aziendali sono ben definite e definite. C'è solo un set di regole aziendali.

In una situazione come questa, raramente avrai implementazioni diverse di una cosa simile, sei limitato dai vincoli di business e questi devono essere ben definiti.

Probabilmente avrai dei test unitari per questo livello e quando modifichi questo livello, vuoi che i test falliscano, inidicando qualcosa (di solito una regola aziendale) è davvero cambiato.

Caso 2.

Quando ha senso estendere una classe

Poi c'è l'altra situazione, che può essere applicata praticamente a tutto il tuo dominio. Servizi, moduli, livello di persistenza, livello di caching, repository, ...

Molte di queste sono classi, che, anche se possono esistere solo una volta nel tuo codice, potrebbero essere sostituite un giorno con un'alternativa migliore. È qui che è utile codificare un'astrazione, che si tratti di un'interfaccia o di una classe astratta che agisce come un supertipo per le implementazioni concrete.

Non dipendendo da un'implementazione concreta ma un'astrazione, quando arriva una nuova alternativa per il servizio (che si tratti di memorizzazione nella cache, persistenza o elaborazione dei dati), puoi semplicemente fornire una nuova implementazione, modificare la configurazione per utilizzare la nuova implementazione e sei praticamente impostato, senza utilizzare il resto del codice (di solito piuttosto grande).

L'OCP sicuramente non ha senso per tutto. Come per ogni modello, anche questo è pensato per facilitare lo sviluppo, ma non è una regola da rispettare obbligatoriamente.

    
risposta data 20.02.2016 - 11:45
fonte
2

Immagino che questa sia una domanda piuttosto complicata, in cui potremmo fare cose diverse in diversi scenari.

1: "Business as Usual" nuovi requisiti per caso d'uso specifico: ci si aspetterebbe una nuova classe figlio, o implementazione di interfaccia senza modifiche alla classe base, e nella mia esperienza questo è generalmente ciò che viene effettivamente fatto.

2: I requisiti BAU cambiano nel tempo per il caso d'uso originale: quindi qui si potrebbe sostenere che l'approccio corretto è quello di lasciare la classe originale e avere classe V2 con le modifiche. Tuttavia, per lo più vedo la classe originale (e test unitari) ) in fase di modifica e controllo delle versioni ottenuto tramite il controllo del codice sorgente.

3: nuovo progetto con requisiti in evoluzione. In questo caso penso che tutti modifichino la classe originale, è ancora in sviluppo, dopo tutto. C'è un po 'di area grigia tra una nuova release di sistema e viene vista come un sistema di lavoro' bedded down '.

4: una libreria di terze parti ha bisogno di personalizzazione. Questo è meno comune ma l'ho visto poche volte in cui le librerie open source che non sono sufficientemente estendibili ottengono la loro fonte scaricata, modificata e inclusa nella soluzione. Questo raggiunge l'obiettivo, ma ovviamente crea un incubo di manutenzione in futuro.

Penso che tu abbia ragione nel chiamare i test unitari e i test in generale come un fattore importante quando consideri quale codice modificare. I sistemi con un solido set di test unitari ti danno molta più sicurezza quando modifichi il codice e rendono più semplice modificare le classi esistenti piuttosto che creare una nuova implementazione (con test associati)

In sistemi consolidati che sono ancora in uso, ci possono essere una grande quantità di avversità al rischio nell'implementazione delle modifiche. Ci saranno spesso domande su "cosa serve ripetere". Se la modifica richiede solo la ridistribuzione di parti del sistema, questo è molto favorito. Se solo a causa del tempo e delle spese di ripetere il test sull'intero sistema!

    
risposta data 20.02.2016 - 11:41
fonte
2

"Codice alle interfacce" non significa in realtà che devi creare un'interfaccia con la parola chiave dell'interfaccia per ogni classe. Ciò che significa è che non devi dipendere dai dettagli di implementazione della classe. Definisci invece un insieme minimo di caratteristiche / vincoli che il tuo cliente può aspettarsi sarà sempre tenuto dalla classe e da eventuali versioni future della classe, questo può essere definito attraverso la parola chiave dell'interfaccia ma può anche essere puramente nella documentazione o in una combinazione di entrambi . Gli utenti della classe dovrebbero dipendere solo da quella "interfaccia" pubblica, piuttosto che dai dettagli di implementazione.

Inoltre, classi piccole (e talvolta non così piccole) il cui utilizzo è completamente racchiuso da un'altra classe più grande, potrebbero non richiedere affatto un'interfaccia documentata. Questa classe interna può essere considerata una classe helper della classe principale ed è un dettaglio di implementazione della classe principale. Se un utente della classe principale non ha bisogno di sapere cosa fa la classe interna o anche se esiste, allora non gliene importa molto se si apportano modifiche importanti alla classe interna.

Che cosa significa "Apri per estensione ma chiuso per modifica" significa che una classe concreta e una versione futura dell'interfaccia potrebbero definire ulteriori promesse oltre a quelle definite dalla sua interfaccia documentata; ma non dovrebbero rimuovere nessuno dei vincoli originariamente definiti dall'interfaccia. In questo modo un codice che conosce solo l'interfaccia sarebbe in grado di utilizzare in modo intercambiabile qualsiasi classe concreta o modifica futura dell'interfaccia e rimanere compatibile con l'evolversi di una classe e le correzioni di bug o le ottimizzazioni vengono aggiunte a una classe e l'interfaccia stessa cambia .

Questo di solito significa che puoi aggiungere vincoli e metodi a un'interfaccia, ma non dovresti mai rimuoverli (o fare molta attenzione quando lo fai).

Inoltre, "closed to modifications" non si riferisce alle modifiche del codice sorgente (dettagli di implementazione di una classe), ma piuttosto alla modifica delle promesse fatte dalla classe e / o dall'interfaccia.

how normal it is to modify the source code quite often if you have well defined unit test case

Molto normale. Finché non si rompono i vincoli / le promesse dell'interfaccia, non c'è nulla di male nella modifica del codice sorgente. Un'interfaccia ben definita ti consentirà di modificare liberamente l'implementazione, perché la tua classe e il consumatore della tua classe hanno un accordo ben definito su ciò che può e non può essere invocato.

Cambiare le promesse dell'interfaccia, tuttavia, dovrebbe essere molto più raro e dovrebbe richiedere considerazioni un po 'più accurate. Se si tratta di un'interfaccia interna solo per un codice applicazione che usi, allora potresti essere in grado di cambiare l'interfaccia. Ma se stai scrivendo una libreria o un framework, cambiare l'interfaccia potrebbe infrangere il codice di altre persone.

Quando si verifica tale rottura, ci sono due possibilità. Se la rottura è dovuta al fatto che le altre persone stanno scrivendo il loro codice contro la tua implementazione piuttosto che l'interfaccia, allora è il loro bug da correggere; ma se è perché il tuo codice non soddisfa la sua interfaccia documentata, allora è il tuo bug da correggere.

    
risposta data 20.02.2016 - 11:21
fonte
1

Il principio open close è importante quando si creano API o librerie pubbliche che verranno utilizzate da altre persone. Situazioni in cui non si ha o meno il controllo sul codice chiamante fa sì che le interfacce cambino, quindi è consigliabile provare a progettarle in modo da evitare la maggior parte di esse.

Quali potenziali "modifiche" possono essere troppo speculative, rischiando problemi con il principio YAGNI (che trovo molto più importante).

Quando controlli anche il codice chiamante e hai dei test di copertura, non darei troppa importanza a questo principio.

    
risposta data 20.02.2016 - 15:11
fonte
0

Il principio OCP si applica principalmente quando esistono codice e applicazioni che non si controllano e che utilizzano la libreria in produzione. Supponi di sviluppare una libreria o un framework o un servizio che viene utilizzato da molti client diversi. Non sai tutti i diversi modi in cui il tuo codice viene utilizzato da questi client. Come farai a sapere se il tuo cambiamento rompe qualcosa?

Puoi smontare a fondo il tuo codice per essere sicuro che una modifica non infrangerà nulla nel tuo codice , ma non puoi mai essere sicuro che una modifica non infranga il codice di altre persone in un altro sistema . Potrebbero fare affidamento su qualche oscuro comportamento che non hai mai nemmeno pensato. Questa è una preoccupazione molto reale quando si sviluppa una libreria o un framework ampiamente utilizzato.

Se la tua libreria è usata solo dal codice che conosci e hai accesso, la modifica piuttosto che l'estensione è solitamente migliore, poiché mantiene il codice più piccolo e più semplice.

    
risposta data 20.02.2016 - 17:30
fonte