Come si progetta una sottoclasse il cui metodo contraddice la sua superclasse? [duplicare]

10

Come faccio a progettare una sottoclasse il cui metodo contraddice la sua superclasse? Diciamo che abbiamo questo esempio:

# Example 1

class Scissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  def right_handed?
    false
  end
end

Supponiamo che l'ereditarietà sia necessaria in questo esempio, ad es. che potrebbero esserci altri metodi oltre a right_handed? che vengono ereditati.

Quindi LeftHandedScissor è una forbice. Il suo metodo right_handed? corrisponde al tipo di valore di firma e di ritorno del metodo della sua superclasse. Tuttavia, il metodo sottoclasse contraddice il valore restituito valore del metodo superclasse.

Questo mi infastidisce perché sembra una contraddizione logica:

  1. Tutte le forbici sono destrorse.
  2. Una forbice mancina è una forbice.
  3. Pertanto, una forbice mancina è destrorsa.

Ho preso in considerazione l'idea di introdurre un AbstractScissor per evitare questa contraddizione:

# Example 2

class AbstractScissor
  def right_handed?
    raise NotImplementedError
  end
end

class RightHandedScissor < AbstractScissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < AbstractScissor
  def right_handed?
    false
  end
end

Il mio collega dice che l'aspetto negativo di questa soluzione è che è più prolisso. Oltre ad aggiungere una classe in più, ci impone di chiamare le forbici "forbici per destrimani", che nessuno fa nel mondo reale.

Questo mi porta alla mia ultima soluzione:

# Example 3

class AbstractScissor
  def right_handed?
    raise NotImplementedError
  end
end

class Scissor < AbstractScissor
  def right_handed?
    true
  end
end

class LeftHandedScissor < AbstractScissor
  def right_handed?
    false
  end
end

È quasi lo stesso dell'esempio 2, ma ho appena ribattezzato RightHandedScissor come Scissor. C'è una possibilità che qualcuno possa pensare che LeftHandedScissor sia una sottoclasse di Scissor basata unicamente sui nomi di classe, ma almeno il codice rende ovvio che non è così.

Qualche altra idea?

    
posta gsmendoza 29.06.2015 - 10:10
fonte

8 risposte

36

Non c'è nulla di sbagliato nel design mostrato nella domanda. Mentre si potrebbe anche introdurre Scissor astratto con due sottoclassi concrete, e forse più chiarezza generale, è anche comune farlo come mostrato (specialmente quando la gerarchia è il risultato di anni di sviluppo incrementale, con Scissor intorno a molto più lungo del concetto di pudore).

" Ok, immagino che la lezione qui sia che non si fanno ipotesi sulle sottoclassi basate esclusivamente sulla superclasse? "

Si fanno tali presupposti basati sui contratti (firme del metodo) della classe base, ma non sulla base delle implementazioni del metodo. In questo caso il contratto dice che right_handed è un metodo booleano che può essere vero o falso, quindi può essere entrambi. Si dovrebbe ignorare il fatto che l'implementazione della classe base restituisce sempre true, specialmente se si è autorizzati a derivare dalla classe base non bloccandola. L'implementazione della classe base è quindi solo predefinita e il metodo right_handed esiste esattamente perché la forbice potrebbe anche essere lasciata.

    
risposta data 29.06.2015 - 11:09
fonte
23

Stai pensando troppo logicamente! Non c'è contraddizione logica perché le definizioni di classe non sono proposizioni logiche.

Se la classe base di Scissor restituisce true, non corrisponde a dire che le forbici all sono destrorse. Significa semplicemente che l'istanza di una forbice è destrorsa a meno che il metodo non venga sovrascritto in una sottoclasse.

    
risposta data 29.06.2015 - 10:30
fonte
16

Non lo fai. È come dire che tutti gli animali sono cani e poi chiedere come far miagolare i gatti invece di abbaiare. Se stavi nominando correttamente le tue classi, la tua classe Scissor dovrebbe essere chiamata RightHandedScissor ; ora ha senso ereditare LeftHandedScissor da RightHandedScissor ?

  • Un possibile approccio è quello di rendere Scissor abstract di classe, e right_handed sarebbe astratto anche (a differenza del tuo esempio 2).

    Quindi potresti avere RightHandedScissor e LeftHandedScissor se ha senso (anche se non riesco a trovare un esempio dove avrebbe senso).

    Il problema principale qui è che ti stai ripetendo. La proprietà right_handed sta riformulando a malapena il tipo dell'oggetto. Questo porta a due problemi:

    1. Alcuni sviluppatori che usano il tuo codice useranno la proprietà right_handed , mentre altri faranno if scissor.is_a?(RightHandedScissor) . Perché non mantenere un solo modo per farlo?

    2. Cosa succede se, un giorno, viene apportata una modifica alle classi e c'è una contraddizione tra ciò che dice la proprietà e il tipo dell'oggetto?

    Se la logica aziendale impone che il tipo sia la posizione corretta per separare le forbici destrorse da mancini, allora o rimuovere la proprietà, o almeno renderne il valore automaticamente calcolato in base al tipo dell'oggetto.

  • Un approccio migliore consiste nell'avere una classe concreta Scissor in cui la proprietà right_handed , e non il tipo effettivo della classe, determina se la forbice è destrorsa o mancina.

    Questo rende più facile gestire il codice: hai due classi in meno e non hai la ridondanza (cioè una proprietà che riformula a malapena il tipo). Se non vuoi che una forbice per mancini diventi destrorsa, puoi prendere in considerazione una proprietà di sola lettura / costante (cioè quella che può essere impostata solo dal costruttore e non può essere modificata dopo di ciò, la terminologia potrebbe cambiare da una lingua a un'altra).

risposta data 29.06.2015 - 10:18
fonte
9

Un'altra opzione è quella di introdurre l'hand-ness come dipendenza con un valore predefinito di right. In pseudocodice qui perché non ho familiarità con Ruby:

class Scissors {
    Scissors(isRightHanded = true) {
        _isRightHanded = isRightHanded
    }

    IsRightHanded() {
        return _isRightHanded
    }
}

class LeftHandedScissors : Scissors {
    LeftHandedScissors() : Scissors(isRightHanded: false) { }
}

In effetti, non hai nemmeno bisogno della classe LeftHandedScissors - puoi semplicemente creare Scissors(isRightHanded: false) quando ti serve una forbice per mancini.

Questo approccio è chiamato preferendo la composizione sull'ereditarietà

    
risposta data 29.06.2015 - 10:49
fonte
4

Riepilogo: il design senza la classe astratta sarà accettabile solo se accuratamente documentato per distinguere i suoi comportamenti astratti e concreti.

Il principio di sostituzione di Liskov è generalmente considerato una "buona cosa". Con LSP, intendo che se type S è un sottotipo di tipo T, allora gli oggetti di tipo S dovrebbero comportarsi come oggetti di tipo T dovrebbero comportarsi .

Non puoi seguire questo principio se non vengono chiarite le aspettative sul comportamento. Questo insieme di aspettative è il cosiddetto "contratto" della classe. Nella maggior parte delle lingue è espresso da una combinazione di firma e documentazione.

Pertanto, se il tuo primo progetto è corretto dipende dai contratti.

non corretta:

class Scissor
  # Returns true because all scissors are right-handed.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Returns false
  def right_handed?
    false
  end
end

accettabile:

class Scissor
  # Returns true for right-handed scissors but false for left-handed ones.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Returns false
  def right_handed?
    false
  end
end

Il codice è lo stesso. Se il design è ragionevole (ovvero segue l'LSP) o meno dipende dai contratti.

Ora è chiaro che, se conosci solo x.kind_of Scissors , allora x.right_handed? potrebbe restituire true o false.

Ma per quanto riguarda true ? Dovrebbe essere documentato? Sì. Una classe che può essere estesa deve essere documentata due volte. Ha bisogno della documentazione del suo comportamento astratto , che è il comportamento che possiamo aspettarci da tutta la sua istanza (diretta o indiretta), e necessita della documentazione del suo comportamento concreto , che è il comportamento che avranno le sue istanze dirette, cioè il comportamento implementato a questo livello della gerarchia dell'ereditarietà e che viene ereditato se non sovrascritto.

Ancora meglio:

class Scissor
  # Abstract behaviour: Returns true for right-handed scissors but false for left-handed ones.
  # Concrete behaviour: Returns true.
  def right_handed?
    true
  end
end

class LeftHandedScissor < Scissor
  # Abstract and concrete behaviour: Returns false
  def right_handed?
    false
  end
end

Ora è chiaro anche - dalla sola documentazione - che, se conosci s instance_of Scissors , allora x.right_handed? restituirà true .

Spesso le persone non si preoccupano di documentare il comportamento concreto, dal momento che, supponendo che non ci siano bug, spesso può essere decodificato dal codice.

Lo stesso non è vero per il comportamento astratto. A meno che non sia documentato, non può essere recuperato dal codice. È compito dei futuri programmatori indovinare quali sono state le tue intenzioni. Gli implementatori del codice client devono indovinare cosa possono assumere sugli oggetti che sono istanze indirette della classe. Gli implementatori di sottoclassi devono indovinare ciò che è e ciò che non è un override ragionevole.

Tutto ciò che ho detto, preferisco la tua soluzione con la classe astratta. Lo considero uno stile migliore. Ma questa è solo una questione di gusti.

Conclusione: il design senza la classe astratta sarà accettabile solo se accuratamente documentato per distinguere i suoi comportamenti astratti e concreti.

    
risposta data 29.06.2015 - 16:34
fonte
3

"Tutte le forbici sono destrorse"? Da dove prendi quest'idea? Il tuo codice esprime solo "le forbici sono destrorse di default". È un valore predefinito, non una decisione di progettazione. Se non ci fosse modo di avere un valore diverso, qual è il punto in cui si programma una funzione di accesso booleano?

    
risposta data 29.06.2015 - 10:16
fonte
2

This bothers me because it looks like a logical contradiction:

All scissors are right-handed.

Non corretto a questo punto. Hai definito le forbici come se avessero tutte le mani in mano e che questo valore predefinito sia destrorso se non superato. Hai non detto che tutte le forbici sono destrorse, il che richiederebbe l'esclusione forzata (come si può fare in alcune lingue, e non in altre).

Rimuovi quella premessa sbagliata e la contraddizione non c'è più.

    
risposta data 29.06.2015 - 14:22
fonte
1

Congratulazioni, hai appena scoperto come appare la violazione del Principio di sostituzione di Liskov , come il primo commentatore ti ha cortesemente indicato.

Per rispondere esattamente alla tua domanda: secondo il principio di cui sopra, non progetta una sottoclasse il cui metodo contraddice la sua superclasse. I motivi di ciò sono spiegati nella letteratura su LSP, Robert Martin, ad esempio, parla molto dei suoi video di apprendimento.

Per risolvere il tuo problema specifico con contraddizioni logiche: le classi della tua applicazione non corrispondono agli oggetti del mondo reale, e non è necessario . Puoi leggere e ascoltare questo da un gruppo di oratori esperti, J. B. Rainsberger, per esempio.

    
risposta data 29.06.2015 - 16:18
fonte

Leggi altre domande sui tag