Cosa significa quando si dice "incapsulare ciò che varia"?

23

Uno dei principi OOP che ho trovato è: -Incapsula ciò che varia.

Capisco qual è il significato letterale della frase, ovvero nascondere ciò che varia. Tuttavia, non so esattamente come contribuirebbe a un design migliore. Qualcuno può spiegarlo usando un buon esempio?

    
posta Haris Ghauri 03.12.2016 - 02:33
fonte

4 risposte

26

Puoi scrivere un codice simile a questo:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

oppure puoi scrivere un codice simile a questo:

pet.speak();

Se ciò che varia è incapsulato, non devi preoccupartene. Ti preoccupi solo di ciò di cui hai bisogno e qualsiasi cosa tu stia utilizzando calcola come fare ciò di cui hai realmente bisogno in base a ciò che varia.

Incapsula ciò che varia e non devi diffondere il codice intorno a ciò che importa di ciò che varia. Basta impostare il pet per essere un certo tipo che sappia parlare come quel tipo e dopo di che puoi dimenticare quale tipo e trattarlo come un animale domestico. Non devi chiedere quale tipo.

Si potrebbe pensare che il tipo sia incapsulato perché è necessario un getter per accedervi. Io non. Getter's non incapsula davvero. Si limitano a chiacchierare quando qualcuno rompe il tuo incapsulamento. Sono un bel decoratore come hook orientato all'aspetto che viene spesso utilizzato come codice di debug. Indipendentemente dal modo in cui lo si suddivide, si espone ancora il tipo.

Potresti guardare questo esempio e pensare che io stia combinando il polimorfismo e l'incapsulamento. Non sono. Sto confondendo "cosa varia" e "dettagli".

Il fatto che il tuo animale domestico sia un cane è un dettaglio. Uno che potrebbe variare per te. Uno che potrebbe non. Ma certamente uno che potrebbe variare da persona a persona. A meno che non crediamo che questo software sarà sempre usato solo dagli amanti dei cani, è intelligente trattare il cane come un dettaglio e incapsularlo. In questo modo alcune parti del sistema sono beatamente inconsapevoli del cane e non verranno influenzate quando ci uniamo con "pappagalli siamo noi".

Dissocia, separa e nasconde i dettagli dal resto del codice. Non lasciare che la conoscenza dei dettagli si diffonda attraverso il tuo sistema e seguirai "incapsula ciò che varia" bene.

    
risposta data 03.12.2016 - 03:57
fonte
13

"Varia" qui significa "può cambiare nel tempo a causa dei mutevoli requisiti". Questo è un principio fondamentale del design: separare e isolare parti di codice o dati che potrebbero dover cambiare separatamente in futuro. Se un singolo requisito cambia, dovrebbe idealmente richiedere solo a noi di cambiare il codice correlato in un unico posto. Ma se il codice base è mal progettato, cioè altamente interconnesso e logico per il requisito distribuito in molti luoghi, allora il cambiamento sarà difficile e avrà un alto rischio di causare effetti inattesi.

Supponiamo che tu abbia un'applicazione che utilizza il calcolo delle imposte sulle vendite in molti luoghi. Se l'aliquota dell'imposta sulle vendite cambia, cosa preferiresti:

  • l'aliquota dell'imposta sulle vendite è un valore letterale codificato ovunque nell'applicazione in cui viene calcolata l'imposta sulle vendite.

  • l'aliquota dell'imposta sulle vendite è una costante globale, che viene utilizzata ovunque nell'applicazione in cui viene calcolata l'imposta sulle vendite.

  • esiste un unico metodo chiamato calculateSalesTax(product) , che è l'unico luogo in cui viene utilizzata l'aliquota dell'imposta sulle vendite.

  • l'aliquota dell'imposta sulle vendite è specificata in un file di configurazione o nel campo del database.

Poiché l'aliquota dell'imposta sulle vendite può variare a causa di una decisione politica indipendente da altri requisiti, preferiamo averla isolata in una configurazione, in modo che possa essere modificata senza influire su alcun codice. Ma è anche concepibile che la logica per il calcolo dell'imposta sulle vendite possa cambiare, ad es. tariffe diverse per prodotti diversi, quindi ci piace anche avere la logica di calcolo incapsulata. La costante globale potrebbe sembrare una buona idea, ma in realtà è negativa, dal momento che potrebbe incoraggiare l'utilizzo dell'imposta sulle vendite in diversi punti del programma anziché in un singolo luogo.

Ora considera un'altra costante, Pi, che viene anche utilizzata in molti punti del codice. Tiene lo stesso principio di progettazione? No, perché Pi non cambierà. L'estrazione in un file di configurazione o nel campo di un database non fa che introdurre complessità inutile (e a parità di condizioni, preferiamo il codice più semplice). Ha senso renderlo una costante globale piuttosto che identificarlo in più punti per evitare incongruenze e migliorare la leggibilità.

Il punto è che, se guardiamo solo a come il programma funziona ora , l'aliquota dell'imposta sulle vendite e il Pi sono equivalenti, entrambe sono costanti. Solo quando consideriamo ciò che può variare in futuro , ci rendiamo conto che dobbiamo trattarli in modo diverso nella progettazione.

Questo principio è in realtà piuttosto profondo, perché significa che devi guardare oltre ciò che il codice base dovrebbe fare oggi e considerare anche le forze esterne che potrebbero causarne il cambiamento, e capire anche le diverse parti interessate dietro i requisiti.

    
risposta data 04.12.2016 - 14:39
fonte
13

Entrambe le risposte attuali sembrano colpire solo parzialmente, e si concentrano su esempi che offuscano l'idea centrale. Anche questo non è (solo) un principio OOP ma un principio di progettazione software in generale.

La cosa che "varia" in questa frase è il codice. Christophe è sul punto di dire che di solito è qualcosa che può variare, cioè spesso anticipare questo. L'obiettivo è di proteggersi dai futuri cambiamenti nel codice. Questo è strettamente correlato a programmazione su un'interfaccia . Tuttavia, Christophe non è corretto per limitare questo a "dettagli di implementazione". In effetti, il valore di questo consiglio è spesso dovuto a cambiamenti nei requisiti .

Questo è solo indirettamente correlato allo stato di incapsulamento, che è ciò a cui credo che David Arno stia pensando. Questo consiglio non sempre (ma spesso lo fa) suggerisce lo stato di incapsulamento, e questo consiglio vale anche per gli oggetti immutabili. In effetti, la semplice denominazione delle costanti è una forma (molto basilare) di incapsulare ciò che varia.

CandiedOrange confonde esplicitamente "ciò che varia" con "dettagli". Questo è solo parzialmente corretto. Sono d'accordo che qualsiasi codice che varia è "dettagli" in un certo senso, ma un "dettaglio" non può variare (a meno che non si definiscano "dettagli" per rendere questo tautologico). Ci possono essere motivi per incapsulare dettagli non variabili, ma questo motto non è uno. In parole povere, se fossi molto fiducioso che "cane", "gatto" e "papero" sarebbero gli unici tipi con cui avresti mai avuto bisogno di gestirli, allora questo motto non suggerisce che il refactoring di CandiedOrange sia eseguito.

Lanciare l'esempio di CandiedOrange in un contesto diverso, supponiamo di avere un linguaggio procedurale come C. Se ho del codice che contiene:

if (pet.type() == dog) {
  pet.bark();
} else if (pet.type() == cat) {
  pet.meow();
} else if (pet.type() == duck) {
  pet.quack()
}

Posso ragionevolmente aspettarmi che questo pezzo di codice cambi in futuro. Posso "incapsulare" semplicemente definendo una nuova procedura:

void speak(pet) {
  if (pet.type() == dog) {
    pet.bark();
  } else if (pet.type() == cat) {
    pet.meow();
  } else if (pet.type() == duck) {
    pet.quack()
  }
}

e usando questa nuova procedura invece del blocco di codice (cioè un refactoring di "metodo di estrazione"). A questo punto, aggiungere un tipo "cow" o qualsiasi altra cosa richiede solo l'aggiornamento della procedura speak . Ovviamente, in un linguaggio OO si può invece sfruttare l'invio dinamico come accennato dalla risposta di CandiedOrange. Questo accadrà naturalmente se accedi a pet tramite un'interfaccia. Eliminare la logica condizionale tramite la spedizione dinamica è una preoccupazione ortogonale che faceva parte del motivo per cui ho realizzato questa versione procedurale. Voglio anche sottolineare che questo non richiede funzionalità particolari per OOP. Anche in un linguaggio OO, incapsulare ciò che varia non significa necessariamente creare una nuova classe o interfaccia.

Come esempio più archetipico (che è più vicino ma non abbastanza OO), diciamo che vogliamo rimuovere i duplicati da una lista. Diciamo che lo implementiamo iterando sulla lista tenendo traccia degli articoli che abbiamo visto finora in un altro elenco e rimuovendo tutti gli elementi che abbiamo visto. È ragionevole presumere che potremmo voler cambiare il modo in cui teniamo traccia degli articoli visti, almeno per motivi di prestazioni. Il motto per incapsulare ciò che varia suggerisce che dovremmo costruire un tipo di dati astratto per rappresentare l'insieme di elementi visti. Il nostro algoritmo è ora definito contro questo tipo di dati Set astratto, e se decidiamo di passare a un albero di ricerca binario, il nostro algoritmo non ha bisogno di cambiare o preoccuparsi. In un linguaggio OO, possiamo usare una classe o un'interfaccia per catturare questo tipo di dati astratti. In una lingua come SML / O'Caml potresti invece acquisire il tipo di dati astratti Set come modulo.

Per un esempio orientato ai requisiti, supponiamo di dover convalidare alcuni campi per quanto riguarda alcune logiche di business. Anche se ora potresti avere dei requisiti specifici, sospetti strongmente che si evolveranno. È possibile incapsulare la logica corrente nella propria procedura / funzione / regola / classe.

Sebbene si tratti di una preoccupazione ortogonale che non fa parte di "incapsulare ciò che varia", è spesso naturale estrapolare, cioè parametrizzare, la logica ormai incapsulata. Questo in genere porta a un codice più flessibile e consente di modificare la logica sostituendo un'implementazione alternativa anziché modificando la logica incapsulata.

    
risposta data 04.12.2016 - 09:36
fonte
11

"Incapsula ciò che varia" si riferisce al nascondimento dei dettagli di implementazione che possono cambiare ed evolvere.

Esempio:

Ad esempio, supponiamo che la classe Course tenga traccia di Students che può registrare (). Puoi implementarlo con LinkedList ed esporre il contenitore per consentire l'iterazione su di esso:

class Course { 
    public LinkedList<Student> Atendees; 
    public bool register (Student s);  
    ...
}

Ma questa non è una buona idea:

  • In primo luogo, la gente potrebbe mancare di buon comportamento e usarla come self-service, aggiungendo direttamente gli studenti alla lista, senza passare attraverso il metodo register ().
  • Ma ancora più fastidioso: questo crea una dipendenza del "codice usando" nei dettagli di implementazione interna della classe usata. Ciò potrebbe impedire future evoluzioni della classe, ad esempio se preferiresti usare una matrice, un vettore, una mappa con il numero di posto o la tua propria struttura di dati persistente.

Se incapsuli ciò che varia (o meglio detto, ciò che può variare), tieni la libertà sia per il codice che usa che per la classe incapsulata di evolvere a vicenda da soli. Questo è il motivo per cui è un principio importante in OOP.

Letture aggiuntive:

risposta data 03.12.2016 - 19:54
fonte

Leggi altre domande sui tag