Refactoring e principio aperto / chiuso


12

Recentemente sto leggendo un sito web sullo sviluppo di codice pulito (non inserisco un link qui perché non è in inglese).

Uno dei principi pubblicizzati da questo sito è il principio chiuso aperto : ogni componente software deve essere aperto per l'estensione e chiuso per modifica. Ad esempio, quando abbiamo implementato e testato una classe, dovremmo modificarla solo per correggere bug o aggiungere nuove funzionalità (ad esempio nuovi metodi che non influenzano quelli esistenti). Le funzionalità e l'implementazione esistenti non devono essere modificate.

Normalmente applico questo principio definendo un'interfaccia Ie una corrispondente classe di implementazione A. Quando la classe Aè diventata stabile (implementata e testata), di solito non la modifico troppo (forse, per niente), cioè

  1. Se arrivano nuovi requisiti (ad es. Prestazioni o un'implementazione totalmente nuova dell'interfaccia) che richiedono grandi modifiche al codice, scrivo una nuova implementazione Be continuo a utilizzarla Afinché Bnon è matura. Quando Bè maturo, tutto ciò che serve è cambiare la modalità di Iistanza.
  2. Se i nuovi requisiti suggeriscono anche una modifica all'interfaccia, definisco una nuova interfaccia I'e una nuova implementazione A'. Così I, Asono congelati e restano l'attuazione del sistema di produzione più a lungo I'e A'non sono abbastanza stabili per sostituirli.

Quindi, alla luce di queste osservazioni, sono rimasto un po 'sorpreso dal fatto che la pagina web abbia poi suggerito l'uso di refactoring complessi , "... perché non è possibile scrivere il codice direttamente nella sua forma finale".

Non esiste una contraddizione / conflitto tra l'applicazione del principio aperto / chiuso e la proposta di utilizzare refactoring complessi come best practice? O l'idea qui è che si possono usare refactoring complessi durante lo sviluppo di una classe A, ma quando quella classe è stata testata con successo dovrebbe essere congelata?

Risposte:


9

Penso al principio Open-Closed come obiettivo progettuale . Se finisci per doverlo violare, significa che il tuo progetto iniziale è fallito, il che è certamente possibile e persino probabile.

Il refactoring significa che stai cambiando il design senza cambiare la funzionalità. Probabilmente stai cambiando il tuo design perché c'è un problema con esso. Forse il problema è che è difficile seguire il principio aperto-chiuso quando si apportano modifiche al codice esistente e si sta tentando di risolverlo.

È possibile che si stia eseguendo un refactoring per rendere possibile l'implementazione della funzionalità successiva senza violare l'OCP quando lo si esegue.


Certamente non dovresti pensare a nessun principio come obiettivo di progettazione . Sono strumenti: non stai rendendo il software bello e teoricamente corretto all'interno, stai cercando di produrre valore per il tuo cliente. È una linea guida , niente di più.
T. Sar,

@ T.Sar Un principio è una linea guida, qualcosa per cui ti sforzi, sono orientati alla manutenibilità e alla scalabilità. A me sembra un obiettivo di design. Non riesco a vedere un principio come uno strumento nel modo in cui vedo un disegno o un quadro come uno strumento.
Tulains Córdova,

@ TulainsCórdova Manutenibilità, prestazioni, correttezza, scalabilità: questi sono obiettivi. Il principio Open-Closed è un mezzo per loro - solo uno dei tanti. Non è necessario spingere qualcosa verso il principio aperto-chiuso se non è applicabile ad esso o potrebbe sminuire gli obiettivi reali del progetto. Non vendi "Open-closedness" a un cliente. Come mera linea guida , non è meglio di una regola empirica che può essere scartata se si finisce per trovare un modo per fare le proprie cose in modo più leggibile e chiaro. Le linee guida sono strumenti, dopo tutto, niente di più.
T. Sar,

@ T.Sar Ci sono così tante cose che non puoi vendere a un cliente ... D'altra parte, sono d'accordo con te in quanto non si devono fare cose che pregiudicano gli obiettivi del progetto.
Tulains Córdova,

9

Il principio Open-Closed è più un indicatore di quanto bene è progettato il tuo software ; non un principio da seguire letteralmente. È anche un principio che ci aiuta a impedirci di cambiare accidentalmente le interfacce esistenti (classi e metodi che chiami e come ti aspetti che funzionino).

L'obiettivo è scrivere software di qualità. Una di queste qualità è l'estensibilità. Ciò significa che è facile aggiungere, rimuovere, modificare il codice con quelle modifiche che tendono ad essere limitate al numero di classi esistenti più pratico. L'aggiunta di nuovo codice è meno rischiosa rispetto alla modifica del codice esistente, quindi a questo proposito Open-Closed è una buona cosa da fare. Ma di quale codice stiamo parlando esattamente? Il crimine di violazione di OC è molto meno quando è possibile aggiungere nuovi metodi a una classe invece di dover modificare quelli esistenti.

OC è frattale . Mele a tutte le profondità del tuo design. Tutti presumono che sia applicato solo a livello di classe. Ma è ugualmente applicabile a livello di metodo o a livello di assieme.

Una violazione troppo frequente di OC al livello appropriato suggerisce che forse è il momento di refactoring . "Livello appropriato" è un giudizio che ha tutto a che fare con il tuo progetto complessivo.

Seguire Open-Closed significa letteralmente che il numero di classi esploderà. Creerai inutilmente ("I" maiuscole). Ti ritroverai con un po 'di funzionalità distribuite tra le classi e dovrai quindi scrivere molto più codice per collegarlo tutto insieme. Ad un certo punto ti renderai conto che cambiare la classe originale sarebbe stato meglio.


2
"Il crimine di violazione di OC è molto meno quando è possibile aggiungere nuovi metodi a una classe invece di dover modificare quelli esistenti.": Per quanto ho capito, l'aggiunta di nuovi metodi non viola affatto il principio OC (aperto per l'estensione) . Il problema sta cambiando i metodi esistenti che implementano un'interfaccia ben definita e quindi hanno già una semantica ben definita (chiusa per modifica). In linea di principio, il refactoring non cambia la semantica, quindi l'unico rischio che posso vedere è l'introduzione di bug in codice già stabile e ben testato.
Giorgio,

1
Ecco la risposta CodeReview che illustra open for extension . Il design di quella classe è estensibile. Al contrario, l'aggiunta di un metodo sta modificando la classe.
radarbob

L'aggiunta di nuovi metodi viola LSP, non OCP.
Tulains Córdova,

1
L'aggiunta di nuovi metodi non viola LSP. Se aggiungi un metodo, hai introdotto una nuova interfaccia @ TulainsCórdova
RubberDuck,

6

Il principio Open-Closed sembra essere un principio apparso prima che il TDD fosse più prevalente. L'idea è che è pericoloso refactoring del codice perché potresti rompere qualcosa, quindi è più sicuro lasciare il codice esistente così com'è e semplicemente aggiungerlo ad esso. In assenza di test questo ha senso. L'aspetto negativo di questo approccio è l'atrofia del codice. Ogni volta che estendi una classe invece di refactoring, finisci con un livello aggiuntivo. Stai semplicemente fissando il codice in alto. Ogni volta che aggiungi più codice, aumenti le possibilità di duplicazione. Immaginare; c'è un servizio nella mia base di codice che voglio usare, trovo che non abbia quello che voglio, quindi creo una nuova classe per estenderla e includere la mia nuova funzionalità. Un altro sviluppatore arriva più tardi e vuole anche usare lo stesso servizio. Sfortunatamente, non mi rendo conto che esiste la mia versione estesa. Codificano rispetto all'implementazione originale ma hanno anche bisogno di una delle funzionalità che ho codificato. Invece di usare la mia versione, ora estendono anche l'implementazione e aggiungono la nuova funzionalità. Ora abbiamo 3 classi, quella originale e due nuove versioni che hanno alcune funzionalità duplicate. Seguire il principio aperto / chiuso e questa duplicazione continuerà ad accumularsi per tutta la durata del progetto, portando a una base di codice inutilmente complessa.

Con un sistema ben collaudato non è necessario soffrire di questa atrofia del codice, è possibile eseguire il refactoring sicuro del codice consentendo al progetto di assimilare nuovi requisiti anziché dover fissare continuamente nuovo codice. Questo stile di sviluppo è chiamato design emergente e porta a basi di codice che sono in grado di mantenersi in buona forma per tutta la loro vita piuttosto che raccogliere gradualmente l'innesto.


1
Non sono un sostenitore del principio aperto-chiuso né del TDD (nel senso che non li ho inventati). Ciò che mi ha sorpreso è stato che qualcuno ha proposto il principio aperto-chiuso E l'uso di refactoring E TDD allo stesso tempo. Questo mi è sembrato contraddittorio e quindi stavo cercando di capire come riunire tutte queste linee guida in un processo coerente.
Giorgio,

"L'idea è che è pericoloso refactoring del codice perché potresti rompere qualcosa, quindi è più sicuro lasciare il codice esistente così com'è e semplicemente aggiungerlo ad esso.": In realtà non lo vedo in questo modo. L'idea è piuttosto quella di avere piccole unità autonome che è possibile sostituire o estendere (consentendo così al software di evolversi), ma non si dovrebbe toccare ciascuna unità una volta che è stata accuratamente testata.
Giorgio,

Devi pensare che la classe non sarà utilizzata solo nella tua base di codice. La libreria che scrivi può essere utilizzata in altri progetti. Quindi OCP è importante. Inoltre, un nuovo programmatore che non conosce una classe estesa con le funzionalità di cui ha bisogno è un problema di comunicazione / documentazione, non un problema di progettazione.
Tulains Córdova,

@ TulainsCórdova nel codice dell'applicazione questo non è rilevante. Per il codice della biblioteca, direi che il versioning semantico era più adatto per comunicare cambiamenti di rottura.
opsb,

1
@ TulainsCórdova con stabilità dell'API del codice della libreria è molto più importante perché non è possibile testare il codice client. Con il codice dell'applicazione la copertura del test ti informerà immediatamente di eventuali rotture. Detto in altro modo, il codice dell'applicazione è in grado di apportare modifiche senza rischi, mentre il codice della libreria deve gestire il rischio mantenendo un'API stabile e segnalando le rotture utilizzando ad esempio il controllo delle versioni semantico
opsb

6

Nelle parole di laici:

A. Principio O / C significa che la specializzazione deve essere fatta estendendo, non modificando una classe per soddisfare esigenze specializzate.

B. L'aggiunta di funzionalità mancanti (non specializzate) significa che il progetto non è stato completato e devi aggiungerlo alla classe base, ovviamente senza violare il contratto. Penso che questo non stia violando il principio.

C. Il refactoring non viola il principio.

Quando un disegno matura , diciamo, dopo un po 'di tempo in produzione:

  • Dovrebbero esserci pochissime ragioni per farlo (punto B), tendendo a zero nel tempo.
  • (Punto C) sarà sempre possibile sebbene più raro.
  • Tutte le nuove funzionalità dovrebbero essere una specializzazione, il che significa che le classi devono essere estese (ereditate da) (punto A).

Il principio aperto / chiuso è molto frainteso. I tuoi punti A e B ottengono esattamente ciò.
gnasher729,

1

Per me, il principio aperto-chiuso è una linea guida, non una regola dura e veloce.

Per quanto riguarda la parte aperta del principio, le classi finali in Java e le classi in C ++ con tutti i costruttori dichiarate private violano la parte aperta del principio aperto-chiuso. Esistono buoni casi d'uso solidi (nota: solido, non SOLIDO) per le classi finali. Progettare per l'estensibilità è importante. Tuttavia, ci vuole molta lungimiranza e impegno, e stai sempre evitando la linea di violazione di YAGNI (non ne avrai bisogno) e iniettando l'odore di codice di generalità speculativa. I componenti software chiave dovrebbero essere aperti per l'estensione? Sì. Tutti? No. Questa in sé è generalità speculativa.

Per quanto riguarda la parte chiusa, quando si va dalla versione 2.0 alla 2.1 alla 2.2 alla 2.3 di alcuni prodotti, non modificare il comportamento è una buona idea. Agli utenti non piace davvero quando ogni versione minore rompe il proprio codice. Tuttavia, lungo la strada si scopre spesso che l'implementazione iniziale nella versione 2.0 era sostanzialmente rotta o che i vincoli esterni che limitavano la progettazione iniziale non erano più applicabili. Sorridi, sopporti e mantieni tale progetto nella versione 3.0, o rendi la 3.0 non compatibile con le versioni precedenti? La compatibilità con le versioni precedenti può essere un enorme limite. I principali limiti di rilascio sono il luogo in cui è accettabile interrompere la compatibilità con le versioni precedenti. Devi stare attento che questo potrebbe far arrabbiare i tuoi utenti. Ci deve essere un buon caso per cui è necessaria questa rottura con il passato.


0

Il refactoring, per definizione, sta cambiando la struttura del codice senza cambiare comportamento. Pertanto, quando si esegue il refactoring, non si aggiungono nuove funzionalità.

Quello che hai fatto come esempio per il principio Open Close sembra OK. Questo principio riguarda l'estensione del codice esistente con nuove funzionalità.

Tuttavia, non sbagliare questa risposta. Non sottintendo che dovresti fare solo funzionalità o eseguire il refactoring solo per grossi blocchi di dati. Il modo più comune di programmare è fare un po 'di una funzionalità piuttosto che fare immediatamente un po' di refactoring (combinato con i test ovviamente per assicurarsi di non aver modificato alcun comportamento). Il refactoring complesso non significa refactoring "grande", significa applicare tecniche di refactoring complicate e ben ponderate.

Informazioni sui principi SOLID. Sono davvero buone linee guida per lo sviluppo del software ma non sono regole religiose da seguire alla cieca. A volte, molte volte, dopo aver aggiunto una seconda, terza e nona funzionalità, ti rendi conto che il tuo progetto iniziale, anche se rispetta Open-Close, non rispetta altri principi o requisiti software. Ci sono punti nell'evoluzione di un design e di un software quando devono essere fatti cambiamenti più complessi. Il punto è trovare e realizzare questi problemi il prima possibile e applicare le tecniche di refactoring nel miglior modo possibile.

Non esiste un design perfetto. Non esiste un progetto del genere che possa e debba rispettare tutti i principi o i modelli esistenti. Questo è codificare l'utopia.

Spero che questa risposta ti abbia aiutato nel tuo dilemma. Sentiti libero di chiedere chiarimenti se necessario.


1
"Quindi, quando si refactoring, non si aggiungono nuove funzionalità.": Ma potrei introdurre bug in un software testato.
Giorgio,

"A volte, molte volte, dopo aver aggiunto una seconda, terza e nona funzionalità, ti rendi conto che il tuo progetto iniziale, anche se rispetta Open-Close, non rispetta altri principi o requisiti software.": È allora che vorrei iniziare a scrivere una nuova implementazione Be, quando è pronta, sostituire la vecchia implementazione Acon la nuova implementazione B(che è un uso delle interfacce). AIl codice può servire come base per Bil codice e quindi posso usare il refactoring sul Bcodice durante il suo sviluppo, ma penso che il Acodice già testato debba rimanere bloccato.
Giorgio,

@Giorgio Quando si refactoring è possibile introdurre bug, ecco perché si scrivono test (o meglio ancora TDD). Il modo più sicuro per refactoring è cambiare codice quando sai che funziona. Lo sai avendo una serie di test che stanno superando. Dopo aver modificato il codice di produzione, i test devono ancora passare, quindi sai che non hai introdotto un bug. E ricordate, i test sono importanti quanto il codice di produzione, quindi applicate loro la stessa regola del codice di produzione e li mantenete puliti e li riformattate periodicamente e frequentemente.
Patkos Csaba,

@Giorgio Se il codice Bè costruito sul codice Acome evoluzione di A, di quando, quando Bviene rilasciato, Adovrebbe essere rimosso e mai più usato. I client che in precedenza utilizzavano Autilizzeranno semplicemente Bsenza conoscere il cambiamento, poiché l'interfaccia Inon è stata modificata (forse un po 'di Principio di sostituzione di Liskov qui? ... la L di SOLID)
Patkos Csaba,

Sì, questo è quello che avevo in mente: non gettare via il codice di lavoro fino a quando non si avrà una sostituzione valida (ben testata).
Giorgio,

-1

Secondo la mia comprensione - se aggiungi nuovi metodi alla classe esistente, allora non si romperà l'OCP. tuttavia sono un po 'confuso con l'aggiunta di nuove variabili nella classe. Ma se si modificano il metodo esistente e i parametri nel metodo esistente, questo sicuramente interromperà l'OCP, perché il codice è già testato e passato se cambiamo intenzionalmente il metodo [Quando cambiano i requisiti], allora sarà un problema.

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.