Sfrutti i vantaggi del principio aperto-chiuso?


12

Il principio open-closed (OCP) afferma che un oggetto dovrebbe essere aperto per l'estensione ma chiuso per modifica. Credo di capirlo e di usarlo insieme a SRP per creare classi che fanno solo una cosa. E, provo a creare molti piccoli metodi che rendono possibile estrarre tutti i controlli comportamentali in metodi che possono essere estesi o sovrascritti in alcune sottoclassi. Quindi, finisco con le classi che hanno molti punti di estensione, sia attraverso: iniezione di dipendenza e composizione, eventi, delega, ecc.

Considera quanto segue una classe semplice ed estensibile:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Ora diciamo, ad esempio, che le OvertimeFactormodifiche a 1.5. Poiché la classe sopra è stata progettata per essere estesa, posso facilmente sottoclassare e restituire una diversa OvertimeFactor.

Ma ... nonostante la classe sia progettata per l'estensione e aderisca a OCP, modificherò il singolo metodo in questione, piuttosto che sottoclassare e sovrascrivere il metodo in questione e quindi ricollegare i miei oggetti nel mio contenitore IoC.

Di conseguenza, ho violato parte di ciò che OCP tenta di realizzare. Mi sembra di essere solo pigro perché quanto sopra è un po 'più facile. Sto fraintendendo OCP? Dovrei davvero fare qualcosa di diverso? Sfruttate i vantaggi dell'OCP in modo diverso?

Aggiornamento : in base alle risposte sembra che questo esempio inventato sia scarso per una serie di ragioni diverse. L'intento principale dell'esempio era dimostrare che la classe era progettata per essere estesa fornendo metodi che, se sottoposti a override, avrebbero modificato il comportamento dei metodi pubblici senza la necessità di modificare il codice interno o privato. Tuttavia, ho sicuramente frainteso l'OCP.

Risposte:


10

Se stai modificando la classe base, allora non è davvero chiusa!

Pensa alla situazione in cui hai rilasciato la biblioteca al mondo. Se vai e cambi il comportamento della tua classe base modificando il fattore degli straordinari su 1,5, hai violato tutte le persone che usano il tuo codice assumendo che la classe fosse chiusa.

Davvero per rendere la classe chiusa ma aperta dovresti recuperare il fattore degli straordinari da una fonte alternativa (file di configurazione forse) o provare un metodo virtuale che può essere ignorato?

Se la lezione fosse veramente chiusa, dopo la tua modifica nessun caso di test fallirebbe (supponendo che tu abbia una copertura del 100% con tutti i tuoi casi di test) e presumo che ci sia un caso di test che controlla GetOvertimeFactor() == 2.0M.

Non oltre l'ingegnere

Ma non portare questo principio di apertura-chiusura alla conclusione logica e avere tutto configurabile dall'inizio (che è oltre l'ingegneria). Definisci solo i bit attualmente necessari.

Il principio chiuso non ti impedisce di riprogettare l'oggetto. Ti preclude dal modificare l'interfaccia pubblica attualmente definita sul tuo oggetto (i membri protetti fanno parte dell'interfaccia pubblica). Puoi comunque aggiungere più funzionalità purché la vecchia funzionalità non sia interrotta.


"Il principio chiuso non ti impedisce di riprogettare l'oggetto." In realtà, lo fa . Se leggi il libro in cui è stato proposto per la prima volta il principio aperto-chiuso, o l' articolo che ha introdotto l'acronimo "OCP", vedrai che dice "Nessuno è autorizzato a apportare modifiche al codice sorgente" (tranne che per il bug correzioni).
Rogério,

@ Rogério: potrebbe essere vero (nel 1988). Ma l'attuale definizione (resa popolare nel 1990 quando OO è diventata popolare) riguarda il mantenimento di un'interfaccia pubblica coerente. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York

Grazie per il riferimento di Wikipedia. Ma non sono sicuro che la definizione "corrente" sia davvero diversa, poiché si basa ancora sull'ereditarietà di tipo (classe o interfaccia). E quella citazione "nessuna modifica del codice sorgente" che ho citato proviene dall'articolo OCP del 1996 di Robert Martin che è (presumibilmente) in linea con la "definizione attuale". Personalmente, penso che il principio aperto-chiuso sarebbe ormai dimenticato, se Martin non gli avesse dato un acronimo che, a quanto pare, ha molto valore commerciale. Il principio stesso è obsoleto e dannoso, IMO.
Rogério,

3

Quindi l'Open Closed Principle è un gotcha ... specialmente se provi ad applicarlo contemporaneamente a YAGNI . Come aderire ad entrambi allo stesso tempo? Applica la regola del tre . La prima volta che apporti una modifica, eseguila direttamente. E anche la seconda volta. La terza volta, è tempo di astrarre quel cambiamento.

Un altro approccio è "ingannami una volta ...", quando devi fare un cambiamento, applica OCP per proteggerti da quel cambiamento in futuro . Vorrei quasi arrivare al punto di proporre che cambiare il tasso di straordinari sia una nuova storia. "Come amministratore delle buste paga voglio cambiare il tasso di lavoro straordinario in modo da poter essere conforme alle leggi sul lavoro applicabili". Ora hai una nuova interfaccia utente per modificare la tariffa degli straordinari, un modo per memorizzarla, e GetOvertimeFactor () chiede semplicemente al suo repository qual è la tariffa degli straordinari.


2

Nell'esempio che hai pubblicato, il fattore di straordinario dovrebbe essere una variabile o una costante. * (Esempio Java)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

O

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Quindi, quando estendi la classe, imposta o ignora il fattore. I "numeri magici" dovrebbero apparire solo una volta. Questo è molto più nello stile di OCP e DRY (Don't Repeat Yourself), perché non è necessario creare una classe completamente nuova per un fattore diverso se si utilizza il primo metodo e si deve solo cambiare la costante in un idiomatico posto nel secondo.

Vorrei usare il primo nei casi in cui ci sarebbero più tipi di calcolatrice, ognuno dei quali necessita di valori costanti diversi. Un esempio potrebbe essere il modello Chain of Responsibility, che di solito viene implementato utilizzando tipi ereditati. Un oggetto che può solo vedere l'interfaccia (cioè getOvertimeFactor()) lo usa per ottenere tutte le informazioni di cui ha bisogno, mentre i sottotipi si preoccupano delle informazioni reali da fornire.

Il secondo è utile nei casi in cui è improbabile che la costante venga modificata, ma viene utilizzata in più posizioni. Avere una costante da cambiare (nel caso improbabile che lo faccia) è molto più facile che impostarla dappertutto o ottenerla da un file delle proprietà.

Il principio Open-closed è meno una chiamata a non modificare l'oggetto esistente che una precauzione per lasciare invariata l'interfaccia a loro. Se hai bisogno di un comportamento leggermente diverso da una classe o di funzionalità aggiuntive per un caso specifico, estendi e sostituisci. Ma se i requisiti per la classe stessa cambiano (come cambiare il fattore), è necessario cambiare la classe. Non ha senso in un'enorme gerarchia di classi, la maggior parte delle quali non viene mai utilizzata.


Questa è una modifica dei dati, non una modifica del codice. Il tasso di lavoro straordinario non avrebbe dovuto essere codificato.
Jim C,

Sembra che tu abbia il tuo Get e il tuo Set al contrario.
Mason Wheeler,

Ops! avrebbe dovuto testare ...
Michael K,

2

Non vedo davvero il tuo esempio come una grande rappresentazione di OCP. Penso che il vero significato della regola sia questo:

Quando si desidera aggiungere una funzione, è necessario aggiungere solo una classe e non è necessario modificare altre classi (ma possibilmente un file di configurazione).

Una cattiva implementazione di seguito. Ogni volta che aggiungi un gioco, dovrai modificare la classe GamePlayer.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

La classe GamePlayer non dovrebbe mai essere modificata

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Ora supponendo che il mio GameFactory rispetti anche OCP, quando voglio aggiungere un altro gioco, avrei solo bisogno di costruire una nuova classe che eredita dalla Gameclasse e tutto dovrebbe funzionare.

Troppo spesso classi come la prima vengono create dopo anni di "estensioni" e non vengono mai refactorizzate correttamente dalla versione originale (o peggio, quelle che dovrebbero essere più classi rimangono una grande classe).

L'esempio fornito è OCP-ish. A mio avviso, il modo corretto di gestire le variazioni delle tariffe degli straordinari sarebbe in un database con le tariffe storiche mantenute in modo che i dati potessero essere rielaborati. Il codice dovrebbe comunque essere chiuso per modifica perché caricherà sempre il valore appropriato dalla ricerca.

Come esempio nel mondo reale, ho usato una variante del mio esempio e il principio aperto-chiuso brilla davvero. La funzionalità è davvero facile da aggiungere perché devo solo derivare da una classe base astratta e il mio "factory" lo prende automaticamente e al "player" non importa quale implementazione concreta restituisca la factory.


1

In questo esempio particolare, hai quello che è noto come un "Valore magico". Essenzialmente un valore hardcoded che può cambiare nel tempo o meno. Cercherò di affrontare l'enigma che esprimi genericamente, ma questo è un esempio del tipo di cosa in cui creare una sottoclasse è più lavoro che cambiare un valore in una classe.

Molto probabilmente, hai specificato il comportamento troppo presto nella tua gerarchia di classi.

Diciamo che abbiamo il PaycheckCalculator. Il OvertimeFactorsarebbe più che probabile essere calettato fuori delle informazioni relative al dipendente. Un dipendente orario può godere di un bonus straordinario, mentre un dipendente stipendiato non verrebbe pagato nulla. Tuttavia, alcuni dipendenti stipendiati otterranno un tempo immediato a causa del contratto a cui stavano lavorando. Puoi decidere che ci sono alcune classi note di scenari retributivi, ed è così che costruiresti la tua logica.

Nella PaycheckCalculatorclasse base lo rendi astratto e specifichi i metodi che ti aspetti. I calcoli di base sono gli stessi, è solo che alcuni fattori vengono calcolati in modo diverso. Dovresti HourlyPaycheckCalculatorquindi implementare il getOvertimeFactormetodo e restituire 1.5 o 2.0, a seconda del caso. Dovresti StraightTimePaycheckCalculatorimplementare il getOvertimeFactorper restituire 1.0. Infine, una terza implementazione sarebbe NoOvertimePaycheckCalculatorquella che implementerebbe il getOvertimeFactorreturn 0.

La chiave è descrivere solo il comportamento nella classe base che si intende estendere. I dettagli di parti dell'algoritmo globale o valori specifici verrebbero compilati da sottoclassi. Il fatto che tu abbia incluso un valore predefinito per la getOvertimeFactorporta alla "correzione" rapida e semplice su una riga invece di estendere la classe come previsto. Sottolinea inoltre il fatto che è necessario uno sforzo per estendere le lezioni. C'è anche uno sforzo per comprendere la gerarchia delle classi nella tua applicazione. Vuoi progettare le tue lezioni in modo tale da ridurre al minimo la necessità di creare sottoclassi e fornire la flessibilità di cui hai bisogno.

Spunti di riflessione: quando le nostre classi incapsulano determinati fattori di dati come OvertimeFactornel tuo esempio, potresti aver bisogno di un modo per estrarre tali informazioni da un'altra fonte. Ad esempio, un file delle proprietà (dato che assomiglia a Java) o un database conterrebbe il valore e PaycheckCalculatoruseresti un oggetto di accesso ai dati per estrarre i tuoi valori. Ciò consente alle persone giuste di modificare il comportamento del sistema senza richiedere una riscrittura del codice.

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.