Come aderire al principio aperto-chiuso in pratica


14

Comprendo l'intento del principio aperto-chiuso. Ha lo scopo di ridurre il rischio di rompere qualcosa che già funziona durante la modifica, dicendoti di provare a estenderlo senza modificarlo.

Tuttavia, ho avuto qualche difficoltà a capire come questo principio viene applicato nella pratica. Per quanto ne so, ci sono due modi per applicarlo. Prima e dopo un possibile cambiamento:

  1. Prima: programma sulle astrazioni e "predire il futuro" il più possibile. Ad esempio, un metodo drive(Car car)dovrà cambiare in caso di Motorcycleaggiunta di s al sistema in futuro, quindi probabilmente viola OCP. Ma il metodo ha drive(MotorVehicle vehicle)meno probabilità di cambiare in futuro, quindi aderisce all'OCP.

    Tuttavia, è abbastanza difficile prevedere il futuro e sapere in anticipo quali modifiche verranno apportate al sistema.

  2. Dopo: quando è necessaria una modifica, estendi una classe invece di modificarne il codice corrente.

La pratica n. 1 non è difficile da capire. Tuttavia è pratica n. 2 che ho difficoltà a capire come applicare.

Per esempio (l'ho preso da un video su YouTube): diciamo che abbiamo un metodo in una classe che accetta CreditCardgli oggetti: makePayment(CraditCard card). Un giornoVoucher vengono aggiunti al sistema. Questo metodo non li supporta, quindi deve essere modificato.

Quando abbiamo implementato il metodo in primo luogo, non siamo riusciti a prevedere il futuro e programmare in termini più astratti (ad esempio makePayment(Payment pay), quindi ora dobbiamo cambiare il codice esistente.

La pratica n. 2 dice che dovremmo aggiungere la funzionalità estendendo invece di modificare. Cosa significa? Dovrei sottoclassare la classe esistente invece di cambiare semplicemente il suo codice esistente? Dovrei creare una sorta di wrapper attorno ad esso solo per evitare di riscrivere il codice?

Oppure il principio non fa nemmeno riferimento a "come modificare / aggiungere correttamente la funzionalità", ma piuttosto a "come evitare di dover prima apportare modifiche (ad esempio, il programma alle astrazioni)?



1
Il principio aperto / chiuso non determina il meccanismo utilizzato. L'ereditarietà è di solito la scelta sbagliata. Inoltre, è impossibile proteggersi da tutti i cambiamenti futuri. È meglio non tentare di prevedere il futuro, ma una volta che è necessario un cambiamento, modificare il design in modo da poter accogliere futuri cambiamenti dello stesso tipo.
Doval,

Risposte:


14

I principi di progettazione devono sempre essere bilanciati gli uni con gli altri. Non puoi prevedere il futuro e la maggior parte dei programmatori lo fa orribilmente quando ci prova. Ecco perché abbiamo la regola del tre , che riguarda principalmente la duplicazione, ma si applica anche al refactoring per qualsiasi altro principio di progettazione.

Quando hai solo un'implementazione di un'interfaccia, non devi preoccuparti molto di OCP, a meno che non sia cristallino dove si verifichino eventuali estensioni. In effetti, spesso si perde la chiarezza quando si cerca di progettare eccessivamente in questa situazione. Quando lo estendi una volta, rifletti per renderlo OCP amichevole se è il modo più semplice e chiaro per farlo. Quando lo estendi a una terza implementazione, ti assicuri di riformattarlo tenendo conto dell'OCP, anche se richiede un piccolo sforzo in più.

In pratica, quando hai solo due implementazioni, il refactoring quando aggiungi un terzo di solito non è troppo difficile. È quando lo lasci crescere oltre quel punto che diventa problematico da mantenere.


1
Grazie per aver risposto. Fammi vedere se capisco quello che stai dicendo: quello che stai dicendo è che dovrei preoccuparmi dell'OCP principalmente dopo essere stato costretto a fare cambiamenti in una classe. Significato: quando si implementa una classe per la prima volta, non dovrei preoccuparmi molto dell'OCP, poiché è comunque difficile prevedere il futuro. Quando ho bisogno di estenderlo / modificarlo per la prima volta, forse è una buona idea rifattorizzare un po 'per essere più flessibile in futuro (più OCP). E per la terza volta ho bisogno di estendere / modificare la classe, è tempo di fare un po 'di refactoring per renderlo più aderente all'OCP. È ciò che intendi?
Aviv Cohn,

1
Questa è l'idea.
Karl Bielefeldt,

2

Penso che stai guardando troppo lontano nel futuro. Risolvi il problema attuale in modo flessibile che aderisce ad aperto / chiuso.

Diciamo che è necessario implementare un drive(Car car)metodo. A seconda della lingua, hai un paio di opzioni.

  • Per le lingue che supportano il sovraccarico (C ++), basta usare drive(const Car& car)

    Ad un certo punto potresti aver bisogno drive(const Motorcycle& motorcycle), ma non interferirà drive(const Car& car). Nessun problema!

  • Per le lingue che non supportano il sovraccarico (obiettivo C), includere il nome del tipo nel metodo -driveCar:(Car *)car.

    Ad un certo punto dopo potresti aver bisogno -driveMotorcycle:(Motorcycle *)motorcycle, ma di nuovo, non interferirà.

Ciò consente drive(Car car)di essere chiuso alle modifiche, ma è aperto all'estensione ad altri tipi di veicoli. Questa pianificazione futura minimalista che ti consente di svolgere il lavoro oggi, ma ti impedisce di bloccarti in futuro.

Cercare di immaginare i tipi più elementari necessari può portare a un regresso infinito. Cosa succede quando vuoi guidare un Segue, una bicicletta o un Jumbo-jet. Come si costruisce un singolo tipo astratto generico in grado di tenere conto di tutti i dispositivi utilizzati e utilizzati per la mobilità?


La modifica di una classe per aggiungere il nuovo metodo viola il principio aperto-chiuso. Il tuo suggerimento elimina anche la possibilità di applicare il Principio di sostituzione di Liskov a tutti i veicoli che possono guidare, eliminando sostanzialmente una delle parti più potenti di OO.
Dunk,

@Dunk Ho basato la mia risposta sul principio polimorfico aperto / chiuso, non sul rigido principio Meyer aperto / chiuso. È consentito aggiornare le classi per supportare nuove interfacce. In questo esempio, l'interfaccia dell'automobile è separata dall'interfaccia della motocicletta. Questi potrebbero essere formalizzati come classi astratte di guida separate per auto e moto che la classe attuatrice potrebbe supportare.
Jeffery Thomas,

@Dunk Il principio di sostituzione di Liskov è utile, ma non è gratuito. Se la specifica originale richiede solo un'auto, la creazione di un veicolo più generico potrebbe non valere il costo aggiuntivo di denaro, tempo e complessità. Inoltre, è improbabile che un veicolo più generico sia perfettamente adatto a gestire sottoclassi non pianificate. O l'interfaccia per una motocicletta dovrà essere inserita nell'interfaccia del veicolo (che è stata progettata per gestire solo l'auto), oppure dovrai modificare il veicolo per gestire la motocicletta (una vera violazione di aperto / chiuso).
Jeffery Thomas,

Il principio di sostituzione di Liskov non viene fornito gratuitamente, ma non comporta costi elevati. E di solito ripaga molto più di quanto sia mai costato molte volte, anche se un'altra sottoclasse non viene mai ereditata da essa nell'applicazione principale. L'applicazione di LSP rende i test automatizzati molto più facili, il che è già una vittoria. Inoltre, anche se certamente non dovresti andare selvaggio e supporre che tutto avrà bisogno di LSP, se stai costruendo un'applicazione e non hai una buona idea di ciò che probabilmente ne avrà bisogno in un futuro futuro, allora non lo fai sapere abbastanza sulla tua applicazione o sul suo dominio.
Dunk,

1
Per quanto riguarda la definizione di OCP. Potrebbero essere i settori in cui ho lavorato, che tendono a richiedere livelli di verifica più elevati rispetto a una normale società commerciale, ma in generale se un file / classe cambia non solo devi ripetere il test del file / classe ma tutto ciò che fa uso di quel file / classe nel test di regressione. Quindi non importa se qualcuno dice che polimorfico aperto / chiuso va bene, cambiare l'interfaccia ha conseguenze ad ampio raggio, quindi non è poi così bello.
Dunk,

2

Comprendo l'intento del principio aperto-chiuso. Ha lo scopo di ridurre il rischio di rompere qualcosa che già funziona durante la modifica, dicendoti di provare a estenderlo senza modificarlo.

Si tratta anche di non rompere tutti gli oggetti che si basano su quel metodo non modificando il comportamento di oggetti già esistenti. Una volta che un oggetto ha pubblicizzato la modifica del comportamento, diventa rischioso poiché si modifica il comportamento noto e previsto dell'oggetto senza sapere esattamente quali altri oggetti si aspettano quel comportamento.

Cosa significa? Dovrei sottoclassare la classe esistente invece di cambiare semplicemente il suo codice esistente?

Sì.

"Accetta solo carte di credito" è definito come parte del comportamento di quella classe, attraverso la sua interfaccia pubblica. Il programmatore ha dichiarato al mondo che il metodo di questo oggetto accetta solo carte di credito. Lo ha fatto usando un nome un po 'non particolarmente chiaro, ma è fatto. Il resto del sistema fa affidamento su questo.

Potrebbe aver avuto senso in quel momento, ma ora se deve cambiare ora dovresti creare una nuova classe che accetta cose diverse dalle carte di credito.

Nuovo comportamento = nuova classe

A parte questo : un buon modo per prevedere il futuro è pensare al nome a cui hai dato un metodo. Hai dato un nome di metodo dal suono davvero generale come makePayment a un metodo con regole specifiche nel metodo su esattamente quale pagamento può effettuare? Questo è un odore di codice. Se hai regole specifiche, queste dovrebbero essere chiarite dal nome del metodo - makePayment dovrebbe essere makeCreditCardPayment. Fallo quando scrivi l'oggetto per la prima volta e altri programmatori ti ringrazieranno.

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.