Sfrutta i vantaggi del principio di open-closed?

11

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

Considera quanto segue una classe semplice ed estensibile:

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

Ora dì, per esempio, che OvertimeFactor cambia in 1.5. Dal momento che la suddetta classe è stata progettata per essere estesa, posso facilmente sottoclasse e restituire un% diverso diOvertimeFactor.

Ma ... nonostante la classe progettata per l'estensione e che aderisce a OCP, modificheremo il singolo metodo in questione, piuttosto che la sottoclasse e la sovrascrittura del metodo in questione e quindi il ricollegamento i miei oggetti nel mio contenitore IoC.

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

Aggiornamento : in base alle risposte sembra che questo esempio forzato sia scadente per una serie di motivi diversi. L'intento principale dell'esempio era dimostrare che la classe era progettata per essere estesa fornendo metodi che, quando sottoposti a override, alteravano il comportamento dei metodi pubblici senza la necessità di cambiare interno o codice privato. Tuttavia, ho sicuramente frainteso OCP.

    
posta Kaleb Pederson 03.02.2011 - 19:24
fonte

5 risposte

9

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

Pensa alla situazione in cui hai rilasciato la biblioteca al mondo. Se vai a cambiare il comportamento della tua classe base modificando il fattore tempo straordinario a 1,5, allora hai violato tutte le persone che usano il tuo codice presumendo che la classe fosse chiusa.

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

Se la classe è stata veramente chiusa, dopo il cambiamento non si verificheranno casi di test (supponendo di avere una copertura del 100% con tutti i casi di test) e suppongo che ci sia un caso di test che controlla GetOvertimeFactor() == 2.0M .

Non superare l'ingegnere

Ma non prendere questo principio di open-close per la conclusione logica e avere tutto configurabile sin dall'inizio (cioè sopra l'ingegneria). Definisci solo i bit attualmente necessari.

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

    
risposta data 03.02.2011 - 19:30
fonte
3

Quindi il principio Open Closed è un trucco ... specialmente se provi ad applicarlo contemporaneamente a YAGNI . Come posso aderire ad entrambi allo stesso tempo? Applica la regola di tre . La prima volta che apporti una modifica, fallo direttamente. E anche la seconda volta. La terza volta, è il momento di astrarre il cambiamento.

Un altro approccio è "ingannami una volta ...", quando devi apportare una modifica, applica l'OCP per proteggerti da quella modifica in futuro . Sarei quasi arrivato a proporre che cambiare il tasso di straordinari sia una nuova storia. "Come amministratore di un libro paga, voglio cambiare il tasso di straordinari in modo da poter essere in regola con le leggi sul lavoro applicabili". Ora hai una nuova interfaccia utente per cambiare la tariffa degli straordinari, un modo per archiviarla e GetOvertimeFactor () chiede al suo repository quale sia il tasso di straordinari.

    
risposta data 03.02.2011 - 22:05
fonte
2

Nell'esempio che hai pubblicato, il fattore orario 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 sostituisci il fattore. I "numeri magici" dovrebbero apparire solo una volta. Questo è molto più nello stile di OCP e DRY (Do not Repeat Yourself), perché non è necessario creare una nuova classe per un fattore diverso se si utilizza il primo metodo, e solo dover cambiare la costante in un idiomatico posto nel secondo.

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

Il secondo è utile nei casi in cui è probabile che la costante non venga modificata, ma viene utilizzata in più posizioni. Avere una costante da modificare (nell'improbabile caso che lo faccia) è molto più facile che impostarla ovunque o recuperarla da un file di proprietà.

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

    
risposta data 03.02.2011 - 19:38
fonte
2

In realtà non vedo il tuo esempio come una grande rappresentazione di OCP. Penso che la vera regola sia questa:

When you want to add a feature, you should only have to add one class and you should not need to modify any other class (but possibly a config file).

Una cattiva implementazione di seguito. Ogni volta che aggiungi un gioco, devi 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 l'OCP, quando voglio aggiungere un altro gioco, vorrei solo creare una nuova classe che erediti dalla classe Game e tutto dovrebbe funzionare.

Troppo spesso le classi come la prima si accumulano dopo anni di "estensioni" e non vengono mai refactored correttamente dalla versione originale (o peggio, ciò che dovrebbe essere più classi rimane una grande classe).

L'esempio fornito è OCP-ish. A mio parere, il modo corretto di gestire le modifiche del tasso di ore straordinarie sarebbe in un database con tassi storici mantenuti in modo che i dati possano essere rielaborati. Il codice dovrebbe essere chiuso per la modifica perché caricherà sempre il valore appropriato dalla ricerca.

Come esempio reale, ho usato una variante del mio esempio e il principio Open-Closed brilla davvero. La funzionalità è davvero facile da aggiungere perché devo solo derivare da una classe base astratta e la mia "fabbrica" la preleva automaticamente e il "giocatore" non si cura di quale implementazione concreta ritorna la fabbrica.

    
risposta data 03.02.2011 - 21:14
fonte
1

In questo particolare esempio, hai il cosiddetto "Valore magico". Essenzialmente un valore hard coded che può o non può cambiare nel tempo. Proverò ad affrontare l'enigma che esprimi genericamente, ma questo è un esempio del tipo di cosa in cui la creazione di una sottoclasse richiede più lavoro rispetto alla modifica di un valore in una classe.

More than likely, you have specified behavior too early in your class hierarchy.

Diciamo che abbiamo PaycheckCalculator . Il OvertimeFactor verrebbe probabilmente escluso dalle informazioni relative al dipendente. Un dipendente orario può godere di un bonus straordinario, mentre un dipendente stipendiato non viene pagato nulla. Eppure, alcuni dipendenti stipendiati otterranno il tempo giusto a causa del contratto a cui stavano lavorando. Potresti decidere che ci sono alcune classi conosciute di scenari di pagamento, ed è così che potresti costruire la tua logica.

Nella classe base% co_de lo rendi astratto e specifica i metodi che ti aspetti. I calcoli fondamentali sono gli stessi, è solo che determinati fattori sono calcolati in modo diverso. Il tuo PaycheckCalculator implementerebbe quindi il metodo HourlyPaycheckCalculator e restituirà 1.5 o 2.0 come potrebbe essere il tuo caso. Il tuo getOvertimeFactor implementerebbe StraightTimePaycheckCalculator per restituire 1.0. Infine una terza implementazione sarebbe un getOvertimeFactor che implementerebbe il NoOvertimePaycheckCalculator per restituire 0.

La chiave è solo descrivere il comportamento nella classe base che si intende estendere. I dettagli di parti dell'algoritmo complessivo o valori specifici verrebbero compilati da sottoclassi. Il fatto che tu abbia incluso un valore predefinito per getOvertimeFactor porta alla "correzione" rapida e semplice su una riga invece di estendere la classe come previsto. Evidenzia anche il fatto che ci sia uno sforzo per estendere le lezioni. C'è anche uno sforzo per comprendere la gerarchia delle classi nella tua applicazione. Desiderate progettare le vostre classi in modo tale da ridurre al minimo la necessità di creare sottoclassi, pur fornendo la flessibilità di cui avete bisogno.

Spunti di riflessione: quando le nostre classi incapsulano determinati fattori di dati come getOvertimeFactor nell'esempio, potrebbe essere necessario un modo per estrarre tali informazioni da un'altra fonte. Ad esempio, un file di proprietà (poiché questo assomiglia a Java) o un database conterrà il valore e il tuo OvertimeFactor utilizzerà un oggetto di accesso ai dati per estrarre i tuoi valori. Ciò consente alle persone giuste di modificare il comportamento del sistema senza richiedere la riscrittura del codice.

    
risposta data 03.02.2011 - 19:53
fonte

Leggi altre domande sui tag