Dato un branco di cavalli, come posso trovare la lunghezza media del corno di tutti gli unicorni?


30

La domanda sopra è un esempio astratto di un problema comune che riscontro nel codice legacy o, più precisamente, problemi derivanti da precedenti tentativi di risoluzione di questo problema.

Mi viene in mente almeno un metodo .NET framework che ha lo scopo di risolvere questo problema, come il Enumerable.OfType<T>metodo. Ma il fatto che alla fine si finisca per interrogare il tipo di un oggetto in fase di esecuzione non è adatto a me.

Oltre a chiedere ad ogni cavallo "Sei un unicorno?" vengono in mente anche i seguenti approcci:

  • Lancia un'eccezione quando si tenta di ottenere la lunghezza di un corno di un non unicorno (espone funzionalità non appropriate per ciascun cavallo)
  • Restituisce un valore predefinito o magico per la lunghezza di un corno di un non unicorno (richiede controlli predefiniti puntati su tutto il codice che vuole sgranocchiare le statistiche del corno su un gruppo di cavalli che potrebbero essere tutti non unicorni)
  • Elimina l'eredità e crea un oggetto separato su un cavallo che ti dice se il cavallo è un unicorno o meno (che potenzialmente sta spingendo lo stesso problema in un livello)

Ho la sensazione che si risponderà meglio con una "non risposta". Ma come affronti questo problema e se dipende, qual è il contesto attorno alla tua decisione?

Sarei anche interessato ad approfondimenti sul fatto che questo problema esista ancora nel codice funzionale (o forse esiste solo in linguaggi funzionali che supportano la mutabilità?)

Questo è stato contrassegnato come possibile duplicato della seguente domanda: Come evitare il downcasting?

La risposta a questa domanda presuppone che si sia in possesso di un HornMeasurerda cui devono essere eseguite tutte le misurazioni del corno. Ma questa è piuttosto un'imposizione su una base di codice che si è formata secondo il principio egualitario secondo cui tutti dovrebbero essere liberi di misurare il corno di un cavallo.

In assenza di HornMeasurer, l'approccio della risposta accettata rispecchia l'approccio basato sulle eccezioni sopra elencato.

C'è stata anche una certa confusione nei commenti sul fatto che cavalli e unicorni siano entrambi equini o se un unicorno sia una sottospecie magica di cavallo. Entrambe le possibilità dovrebbero essere prese in considerazione - forse una è preferibile all'altra?


22
I cavalli non hanno le corna, quindi la media non è definita (0/0).
Scott Whitlock,

3
@moarboilerplate Ovunque da 10 a infinito.
tata

4
@StephenP: Non funzionerebbe matematicamente in questo caso; tutti quegli 0 distorcerebbero la media.
Mason Wheeler,

3
Se è meglio rispondere alla tua domanda con una non risposta, non appartiene a un sito di domande e risposte; reddit, quora o altri siti basati sulla discussione sono creati per cose di tipo non-risposta ... detto questo, penso che potrebbe essere chiaramente rispondente se stai cercando il codice che @MasonWheeler ha dato, se non penso di non avere idea cosa stai cercando di chiedere ..
Jimmy Hoffa,

3
@JimmyHoffa "stai sbagliando" sembra essere una "non risposta" accettabile e spesso meglio di "beh, ecco un modo in cui potresti farlo" - non è richiesta una discussione estesa.
moarboilerplate

Risposte:


11

Supponendo che tu voglia trattare un Unicorncome un tipo speciale di Horse, ci sono fondamentalmente due modi in cui puoi modellarlo. Il modo più tradizionale è la relazione della sottoclasse. Puoi evitare di controllare il tipo e il downcast semplicemente rifattorizzando il codice per mantenere sempre separati gli elenchi nei contesti in cui è importante e combinarli solo in contesti in cui non ti interessano mai i Unicorntratti. In altre parole, lo organizzi in modo da non entrare mai nella situazione in cui devi estrarre unicorni da una mandria di cavalli in primo luogo. All'inizio questo sembra difficile, ma è possibile nel 99,99% dei casi e di solito rende il codice molto più pulito.

L'altro modo in cui potresti modellare un unicorno è semplicemente dando a tutti i cavalli una lunghezza del corno opzionale. Quindi potresti verificare se è un unicorno controllando se ha una lunghezza del corno e trovare la lunghezza media del corno di tutti gli unicorni di (in Scala):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

Questo metodo ha il vantaggio di essere più semplice, con una singola classe, ma lo svantaggio di essere molto meno estensibile e di avere una sorta di modo indiretto di verificare "unicorno". Il trucco se si utilizza questa soluzione è riconoscere quando si inizia a estenderlo spesso che è necessario passare a un'architettura più flessibile. Questo tipo di soluzione è molto più popolare nei linguaggi funzionali in cui hai funzioni semplici e potenti come flatMapfiltrare facilmente gli Noneelementi.


7
Naturalmente, questo presuppone che l'unica differenza tra un cavallo normale e un unicorno sia il corno. In caso contrario, le cose si complicano molto più rapidamente.
Mason Wheeler,

@MasonWheeler solo nel secondo metodo presentato.
moarboilerplate

1
Rifletti sui commenti su come i non unicorni e gli unicorni non dovrebbero mai essere messi insieme in uno scenario di ereditarietà finché non ti trovi in ​​un contesto in cui non ti importa degli unicorni. Certo, .OfType () potrebbe risolvere il problema e far funzionare le cose, ma si tratta di risolvere un problema che non dovrebbe nemmeno esistere in primo luogo. Per quanto riguarda il secondo approccio, funziona perché le opzioni sono di gran lunga superiori al fare affidamento su null per implicare qualcosa. Penso che il secondo approccio possa essere raggiunto in OO con un compromesso se incapsuli i tratti di unicorno in una proprietà autonoma e sei estremamente vigile.
Moarboilerplate

1
scendere a compromessi se si incapsulano i tratti di unicorno in una proprietà autonoma e si è estremamente vigili - perché rendere la vita difficile per te stesso. Usa direttamente typeof e risparmia un sacco di problemi futuri.
gbjbaanb,

@gbjbaanb Considererei questo approccio davvero appropriato solo per gli scenari in cui un'anemica Horseaveva una IsUnicornproprietà e un qualche tipo di UnicornStuffproprietà con la lunghezza del corno su di essa (quando si ridimensiona per il cavaliere / glitter menzionato nella tua domanda).
Moarboilerplate

38

Hai praticamente coperto tutte le opzioni. Se hai un comportamento che dipende da un sottotipo specifico ed è mescolato con altri tipi, il tuo codice deve essere consapevole di quel sottotipo; questo è semplice ragionamento logico.

Personalmente, ci andrei e basta horses.OfType<Unicorn>().Average(u => u.HornLength). Esprime chiaramente l'intenzione del codice, che è spesso la cosa più importante poiché qualcuno finirà per mantenerlo in seguito.


Per favore, perdonami se la mia sintassi lambda non è corretta; Non sono un gran programmatore di C # e non posso mai mantenere i dettagli arcani come questo direttamente. Dovrebbe essere chiaro cosa intendo, però.
Mason Wheeler,

1
Non preoccuparti, il problema è praticamente risolto una volta che l'elenco contiene solo Unicorns comunque (per la cronaca potresti ometterlo return).
moarboilerplate

4
Questa è la risposta che vorrei cercare se volessi risolvere rapidamente il problema. Ma non la risposta se volessi riformattare il codice per renderlo più plausibile.
Andy,

6
Questa è sicuramente la risposta a meno che non sia necessario un livello assurdo di ottimizzazione. La chiarezza e la leggibilità rendono praticamente tutto il resto discutibile.
David dice di reintegrare Monica il

1
@DavidGrinberg e se scrivere questo metodo pulito e leggibile significasse che dovresti prima implementare una struttura ereditaria che prima non esisteva?
moarboilerplate

9

Non c'è niente di sbagliato in .NET con:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

Anche l'uso dell'equivalente Linq va bene:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

Sulla base della domanda che hai posto nel titolo, questo è il codice che mi aspetterei di trovare. Se la domanda ponesse qualcosa del tipo "qual è la media degli animali con le corna" sarebbe diverso:

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Si noti che quando si utilizza Linq, Average(e Mine Max) genererà un'eccezione se l'enumerabile è vuoto e il tipo T non è nullable. Questo perché la media è davvero indefinita (0/0). Quindi hai davvero bisogno di qualcosa del genere:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

modificare

Penso solo che questo debba aggiungere ... uno dei motivi per cui una domanda come questa non si adatta bene ai programmatori orientati agli oggetti è che presume che stiamo usando classi e oggetti per modellare una struttura di dati. L'idea originale orientata agli oggetti Smalltalk-esque era quella di strutturare il programma in base a moduli che erano stati istanziati come oggetti ed eseguiti servizi per te quando hai inviato loro un messaggio. Il fatto che possiamo anche usare classi e oggetti per modellare una struttura di dati è un (utile) effetto collaterale, ma sono due cose diverse. Non penso nemmeno che quest'ultimo debba essere considerato una programmazione orientata agli oggetti, dal momento che potresti fare la stessa cosa con a struct, ma non sarebbe altrettanto carino.

Se stai usando la programmazione orientata agli oggetti per creare servizi che fanno le cose per te, allora chiederti se quel servizio è in realtà un altro servizio o un'implementazione concreta è generalmente disapprovato per buoni motivi. Ti è stata fornita un'interfaccia (in genere tramite l'iniezione delle dipendenze) e dovresti codificare tale interfaccia / contratto.

D'altra parte, se stai usando (erroneamente) le idee di classe / oggetto / interfaccia per creare una struttura di dati o un modello di dati, allora personalmente non vedo alcun problema con l'utilizzo dell'is-un'idea al massimo. Se hai definito che gli unicorni sono un sottotipo di cavalli e ha perfettamente senso all'interno del tuo dominio, allora vai assolutamente avanti e interroga i cavalli nella tua mandria per trovare gli unicorni. Dopotutto, in un caso come questo, in genere stiamo cercando di creare un linguaggio specifico di dominio per esprimere meglio le soluzioni ai problemi che dobbiamo risolvere. In questo senso non c'è niente di sbagliato in .OfType<Unicorn>()ecc.

In definitiva, prendere una raccolta di elementi e filtrarla in base al tipo è in realtà solo una programmazione funzionale, non una programmazione orientata agli oggetti. Per fortuna linguaggi come C # ora sono a proprio agio nel gestire entrambi i paradigmi.


7
Sai già che animal è un Unicorn ; cast piuttosto che utilizzare as, o potenzialmente anche meglio usare as e quindi verificare la presenza di null.
Philip Kendall,

3

Ma il fatto che alla fine si finisca per interrogare il tipo di un oggetto in fase di esecuzione non è adatto a me.

Il problema con questa affermazione è che, indipendentemente dal meccanismo che usi, interrogherai sempre l'oggetto per dire che tipo è. Che può essere RTTI o può essere un'unione o una semplice struttura di dati in cui lo chiediif horn > 0 . Le specifiche esatte cambiano leggermente ma l'intento è lo stesso: chiedi all'oggetto su se stesso in qualche modo per vedere se dovresti interrogarlo ulteriormente.

Detto questo, ha senso utilizzare il supporto della tua lingua per farlo. In .NET userestitypeof ad esempio.

Il motivo per farlo va oltre il semplice uso della tua lingua. Se hai un oggetto che assomiglia a un altro, ma per qualche piccolo cambiamento, è probabile che nel tempo troverai più differenze. Nel tuo esempio di unicorni / cavalli potresti dire che c'è solo la lunghezza di un corno ... ma domani controllerai se un potenziale cavaliere è vergine o se la cacca è luccicante. (un classico esempio del mondo reale sarebbero i widget della GUI che derivano da una base comune e dovresti cercare caselle di controllo e caselle di riepilogo in modo diverso. Il numero di differenze sarebbe troppo grande per creare semplicemente un singolo super oggetto che contenesse tutte le possibili permutazioni di dati ).

Se il controllo del tipo di un oggetto in fase di runtime non tiene bene, la tua alternativa è quella di dividere i diversi oggetti fin dall'inizio - invece di memorizzare un singolo branco di unicorni / cavalli, hai 2 raccolte - una per cavalli, una per unicorni . Questo può funzionare molto bene, anche se li memorizzi in un contenitore specializzato (ad es. Una multimappa in cui la chiave è il tipo di oggetto ... ma poi anche se li memorizziamo in 2 gruppi, siamo subito tornati a interrogare il tipo di oggetto !)

Certamente un approccio basato sull'eccezione è sbagliato. Usare le eccezioni come normale flusso di programma è un odore di codice (se avevi una mandria di unicorni e un asino con una conchiglia attaccata alla testa intrufolata, allora direi che l'approccio basato sulle eccezioni è OK, ma se hai una mandria di unicorni e i cavalli che controllano ciascuno per unicorno non è inaspettato. Le eccezioni sono per circostanze eccezionali, non una ifdichiarazione complicata ). In ogni caso, utilizzare le eccezioni per questo problema è semplicemente interrogare il tipo di oggetto in fase di esecuzione, solo qui si sta abusando della funzionalità del linguaggio per verificare la presenza di oggetti non unicorno. Puoi anche scrivere il codice in aif horn > 0 e almeno elabora la tua raccolta in modo rapido, chiaro, usando meno righe di codice ed evitando qualsiasi problema derivante dal lancio di altre eccezioni (ad esempio una raccolta vuota o cercando di misurare la conchiglia di quell'asino)


In un contesto legacy, if horn > 0è praticamente il modo in cui questo problema viene risolto all'inizio. Quindi i problemi che sorgono di solito sono quando vuoi controllare cavalieri e glitter, ed horn > 0è sepolto dappertutto in un codice non correlato (anche il codice soffre di bug misteriosi a causa della mancanza di controlli per quando horn è 0). Inoltre, sottoclassare il cavallo dopo il fatto è di solito la proposta più costosa, quindi di solito non sono propenso a farlo se sono ancora scritti insieme alla fine del refactor. Quindi diventa certamente "quanto sono brutte le alternative"
moarboilerplate

@moarboilerplate lo dici tu stesso, vai con la soluzione economica e semplice e si trasformerà in un pasticcio. Ecco perché sono state inventate le lingue OO, come soluzione a questo tipo di problema. all'inizio il sottoclasse di cavalli potrebbe sembrare costoso, ma presto si ripaga da solo. Continuare con la soluzione semplice, ma fangosa, costa sempre di più nel tempo.
gbjbaanb,

3

Dato che la domanda ha un functional-programmingtag, potremmo usare un tipo di somma per riflettere i due gusti di cavalli e la corrispondenza dei modelli per chiarire le differenze. Ad esempio, in F #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

Su OOP, FP ha il vantaggio della separazione di dati / funzioni, che forse ti salva dalla (ingiustificata?) "Coscienza colpevole" di violare il livello di astrazione durante il downcasting a sottotipi specifici da un elenco di oggetti di un supertipo.

Contrariamente alle soluzioni OO proposte in altre risposte, la corrispondenza dei modelli fornisce anche un punto di estensione più semplice qualora un Equinegiorno si presentassero altre specie di cornuti .


2

La forma abbreviata della stessa risposta alla fine richiede la lettura di un libro o di un articolo web.

Modello visitatore

Il problema ha una miscela di cavalli e unicorni. (La violazione del principio di sostituzione di Liskov è un problema comune nelle basi di codici legacy.)

Aggiungi un metodo a cavallo e tutte le sottoclassi

Horse.visit(EquineVisitor v)

L'interfaccia del visitatore equino è simile a questa in java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Per misurare le corna ora scriviamo ....

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

Il modello dei visitatori è criticato per aver reso più difficile il refactoring e la crescita.

Risposta breve: utilizzare il modello di progettazione Visitatore per ottenere una doppia spedizione.

vedi anche https://en.wikipedia.org/wiki/Visitor_pattern

vedi anche http://c2.com/cgi/wiki?VisitorPattern per la discussione dei visitatori.

vedi anche Design Patterns di Gamma et al.


Stavo per rispondere con il modello di visitatore me stesso. Ho dovuto scorrere un modo sorprendente per scoprire se qualcuno lo avesse già menzionato!
Ben Thurley,

0

Supponendo che nella tua architettura gli unicorni siano una sottospecie di cavalli e incontri luoghi in cui ottieni una raccolta in Horsecui alcuni di essi potrebbero essere Unicorn, andrei personalmente con il primo metodo ( .OfType<Unicorn>()...) perché è il modo più semplice per esprimere la tua intenzione . Per chiunque arrivi dopo (incluso te stesso in 3 mesi), è immediatamente ovvio cosa stai cercando di ottenere con quel codice: scegli gli unicorni tra i cavalli.

Gli altri metodi che hai elencato sembrano solo un altro modo di porre la domanda "Sei un unicorno?". Ad esempio, se usi una sorta di metodo basato sull'eccezione per misurare i clacson, potresti avere un codice simile a questo:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

Quindi ora l'eccezione diventa l'indicatore che qualcosa non è un unicorno. E ora questa non è più una situazione eccezionale , ma fa parte del normale flusso del programma. E usare un'eccezione invece di una ifsembra ancora più sporco del semplice controllo del tipo.

Diciamo che segui la via del valore magico per controllare le corna sui cavalli. Quindi ora le tue lezioni hanno un aspetto simile al seguente:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Ora la tua Horseclasse deve conoscere la Unicornclasse e avere metodi extra per affrontare le cose che non le interessano. Ora immaginate di avere anche Pegasuss e Zebras che ereditano da Horse. Ora ha Horsebisogno di un Flymetodo così come MeasureWings, CountStripesecc. E poi anche la Unicornclasse ottiene questi metodi. Ora tutte le tue classi devono conoscersi a vicenda e hai inquinato le classi con un mucchio di metodi che non dovrebbero essere lì solo per evitare di chiedere al sistema di tipi "È un unicorno?"

Che dire di aggiungere qualcosa a Horses per dire se qualcosa è Unicornae gestire tutte le misurazioni del corno? Bene, ora devi verificare l'esistenza di questo oggetto per sapere se qualcosa è un unicorno (che sta semplicemente sostituendo un controllo con un altro). Inoltre confonde un po 'le acque in quanto ora potresti avere unList<Horse> unicornsche contiene davvero tutti gli unicorni, ma il sistema di tipi e il debugger non possono facilmente dirtelo. "Ma so che sono tutti gli unicorni" dici "il nome lo dice anche". Bene, e se qualcosa fosse mal chiamato? O dire, hai scritto qualcosa con il presupposto che sarebbero davvero tutti gli unicorni, ma poi i requisiti sono cambiati e ora potrebbe anche aver mescolato pegasi? (Perché non succede mai nulla di simile, specialmente nel software legacy / sarcasmo.) Ora il sistema dei tipi inserirà felicemente il tuo pegasi con i tuoi unicorni. Se la tua variabile fosse stata dichiarata come List<Unicorn>compilatore (o ambiente di runtime) sarebbe adatta se provassi a mescolare in pegasi o cavalli.

Infine, tutti questi metodi sono solo una sostituzione del controllo del sistema dei tipi. Personalmente, preferirei non reinventare la ruota qui e spero che il mio codice funzioni altrettanto bene come qualcosa che è integrato ed è stato testato da migliaia di altri programmatori migliaia di volte.

In definitiva, il codice deve essere comprensibile per te . Il computer lo capirà indipendentemente da come lo scrivi. Sei tu quello che deve eseguire il debug ed essere in grado di ragionare su di esso. Fai la scelta che semplifica il tuo lavoro. Se per qualche motivo, uno di questi altri metodi offre un vantaggio per te che supera il codice più chiaro nei punti in cui verrebbe visualizzato, provalo. Ma ciò dipende dalla tua base di codice.


L'eccezione silenziosa è decisamente negativa - la mia proposta era un controllo che sarebbe stato if(horse.IsUnicorn) horse.MeasureHorn();e le eccezioni non sarebbero state colte - sarebbero state innescate quando !horse.IsUnicorne tu ti trovi in ​​un contesto di misurazione di un unicorno o all'interno di MeasureHornun non-unicorno. In questo modo, quando viene generata l'eccezione, non mascherate gli errori, esplode completamente ed è un segno che qualcosa deve essere risolto. Ovviamente è appropriato solo per determinati scenari, ma è un'implementazione che non utilizza il lancio di eccezioni per determinare un percorso di esecuzione.
Moarboilerplate

0

Bene, sembra che il tuo dominio semantico abbia una relazione IS-A, ma sei un po 'diffidente nell'usare sottotipi / ereditarietà per modellarlo, in particolare a causa della riflessione del tipo di runtime. Penso tuttavia che tu abbia paura della cosa sbagliata: il sottotitolo comporta effettivamente dei pericoli, ma il fatto che stai interrogando un oggetto in fase di esecuzione non è il problema. Vedrai cosa intendo.

La programmazione orientata agli oggetti si è appoggiata piuttosto pesantemente alla nozione di relazioni IS-A, si è probabilmente appoggiata troppo pesantemente su di essa, portando a due famosi concetti critici:

Ma penso che ci sia un altro modo più basato sulla programmazione funzionale per guardare alle relazioni IS-A che forse non ha queste difficoltà. Innanzitutto, vogliamo modellare cavalli e unicorni nel nostro programma, quindi avremo un Horsee un Unicorntipo. Quali sono i valori di questi tipi? Bene, direi questo:

  1. I valori di questi tipi sono rappresentazioni o descrizioni di cavalli e unicorni (rispettivamente);
  2. Sono rappresentazioni o descrizioni schematizzate : non sono in forma libera, sono costruite secondo regole molto rigide.

Ciò può sembrare ovvio, ma penso che uno dei modi in cui le persone affrontano problemi come il problema dell'ellisse circolare sia non badare a questi punti con sufficiente attenzione. Ogni cerchio è un'ellisse, ma ciò non significa che ogni descrizione schematizzata di un cerchio sia automaticamente una descrizione schematizzata di un'ellisse secondo uno schema diverso. In altre parole, solo perché un cerchio è un'ellisse non significa che a Circlesia un Ellipse, per così dire. Ma significa che:

  1. C'è una funzione totale che converte qualsiasi Circle(descrizione del cerchio schematizzata) in un Ellipse(diverso tipo di descrizione) che descrive gli stessi cerchi;
  2. Esiste una funzione parziale che accetta un Ellipsee, se descrive un cerchio, restituisce il corrispondente Circle.

Quindi, in termini di programmazione funzionale, il tuo Unicorntipo non deve necessariamente essere un sottotipo Horse, devi solo operazioni come queste:

-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse

-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn

E toUnicornha bisogno di essere un'inversa destra di toHorse:

toUnicorn (toHorse x) = Just x

Il Maybetipo di Haskell è ciò che altre lingue chiamano un tipo di "opzione". Ad esempio, il Optional<Unicorn>tipo Java 8 è o Unicorno niente. Nota che due delle tue alternative - lanciare un'eccezione o restituire un "valore predefinito o magico" - sono molto simili ai tipi di opzione.

Quindi sostanzialmente quello che ho fatto qui è ricostruire il concetto di relazione IS-A in termini di tipi e funzioni, senza usare sottotipi o ereditarietà. Quello che vorrei togliere da questo è:

  1. Il tuo modello deve avere un Horsetipo;
  2. Il Horsetipo deve codificare informazioni sufficienti per determinare in modo inequivocabile se un valore descrive un unicorno;
  3. Alcune operazioni del Horsetipo devono esporre tali informazioni in modo che i client del tipo possano osservare se un dato Horseè un unicorno;
  4. I client del Horsetipo dovranno utilizzare queste ultime operazioni in fase di esecuzione per discriminare tra unicorni e cavalli.

Quindi questo è fondamentalmente un modello "chiedi a tutti Horsese si tratta di un unicorno". Sei diffidente nei confronti di quel modello, ma penso che sia sbagliato. Se ti do un elenco di Horses, tutto ciò che il tipo garantisce è che le cose che descrivono gli elementi nell'elenco sono cavalli - quindi, inevitabilmente, dovrai fare qualcosa in fase di esecuzione per dire quali di loro sono unicorni. Quindi non c'è niente da fare, penso - devi implementare operazioni che lo faranno per te.

Nella programmazione orientata agli oggetti, il modo familiare per farlo è il seguente:

  • Avere un Horsetipo;
  • Avere Unicorncome sottotipo di Horse;
  • Utilizzare la riflessione del tipo di runtime come operazione accessibile al client che discute se un dato Horseè un Unicorn.

Questo ha un grosso punto debole, se lo guardi dal punto di vista della "cosa contro la descrizione" che ho presentato sopra:

  • E se hai Horseun'istanza che descrive un unicorno ma non è Unicornun'istanza?

Tornando all'inizio, questa è quella che penso sia la parte davvero spaventosa dell'uso di sottotipi e downcast per modellare questa relazione IS-A, non il fatto che devi fare un controllo di runtime. Abusare un po 'della tipografia, chiedere a Horsese si tratta di Unicornun'istanza non è sinonimo di chiedere a Horsese si tratta di un unicorno (se si tratta di una Horsedescrizione di un cavallo che è anche un unicorno). A meno che il tuo programma non abbia fatto di tutto per incapsulare il codice che costruisce in Horsesmodo tale che ogni volta che un client tenta di costruire un Horseche descrive un unicorno, la Unicornclasse viene istanziata. Nella mia esperienza, raramente i programmatori fanno le cose con attenzione.

Quindi andrei con l'approccio in cui esiste un'operazione esplicita, non downcast, che converte da Horses a Unicorns. Questo potrebbe essere un metodo del Horsetipo:

interface Horse {
    // ...
    Optional<Unicorn> toUnicorn();
}

... o potrebbe essere un oggetto esterno (il tuo "oggetto separato su un cavallo che ti dice se il cavallo è un unicorno o no"):

class HorseToUnicornCoercion {
    Optional<Unicorn> convert(Horse horse) {
       // ...
    }
}

La scelta tra queste è una questione di come è organizzato il tuo programma: in entrambi i casi, hai l'equivalente della mia Horse -> Maybe Unicornoperazione dall'alto, lo stai solo impacchettando in diversi modi (che sicuramente avrà effetti a catena su quali operazioni il Horsetipo ha bisogno per esporre ai propri clienti).


-1

Il commento di OP in un'altra risposta ha chiarito la domanda, ho pensato

fa parte anche di quello che la domanda sta ponendo. Se ho una mandria di cavalli, e alcuni di loro sono concettualmente unicorni, come dovrebbero esistere in modo che il problema possa essere risolto in modo pulito senza troppi impatti negativi?

Frase in questo modo, penso che abbiamo bisogno di ulteriori informazioni. La risposta probabilmente dipende da una serie di cose:

  • Le nostre strutture linguistiche. Ad esempio, probabilmente lo approccerei diversamente in ruby, javascript e Java.
  • I concetti stessi: Che cosa è un cavallo e che cosa è un unicorno? Quali dati sono associati a ciascuno? Sono esattamente gli stessi, tranne il corno, o hanno altre differenze?
  • In quale altro modo li stiamo usando, oltre a prendere le medie della lunghezza del corno? E le mandrie? Forse dovremmo modellarli anche noi? Li usiamo altrove? herd.averageHornLength()sembra corrispondere al nostro modello concettuale.
  • Come vengono creati gli oggetti cavallo e unicorno? La modifica di tale codice rientra nei limiti del nostro refactoring?

In generale, tuttavia, non vorrei nemmeno pensare all'eredità e ai sottotipi qui. Hai un elenco di oggetti. Alcuni di quegli oggetti possono essere identificati come unicorni, forse perché hanno unhornLength() metodo. Filtra l'elenco in base a questa proprietà unica unicorno. Ora il problema è stato ridotto alla media della lunghezza del corno di un elenco di unicorni.

OP, fammi sapere se sto ancora fraintendendo ...


1
Punti giusti. Per evitare che il problema diventi ancora più astratto, dobbiamo fare alcune supposizioni ragionevoli: 1) un linguaggio fortemente tipizzato 2) la mandria limita i cavalli a un tipo, probabilmente a causa di una raccolta 3) tecniche come la tipizzazione delle anatre dovrebbero probabilmente essere evitate . Per quanto riguarda ciò che può essere modificato, non ci sono necessariamente limitazioni, ma ogni tipo di cambiamento ha le sue conseguenze uniche ...
moarboilerplate

Se la mandria vincola i cavalli a un tipo, non sono le nostre uniche scelte ereditarie (non mi piace quell'opzione) o un oggetto wrapper (diciamo HerdMember) che inizializziamo con un cavallo o un unicorno (liberando cavallo e unicorno dal bisogno di una relazione di sottotipo ). HerdMemberè quindi libero da implementare isUnicorn()come meglio ritiene, e la soluzione di filtro che suggerisco segue.
Giona

In alcune lingue, hornLength () può essere mischiato e, in tal caso, può essere una soluzione valida. Tuttavia, nelle lingue in cui la digitazione è meno flessibile, devi ricorrere ad alcune tecniche di hacking per fare la stessa cosa, oppure devi fare qualcosa come mettere la lunghezza del corno su un cavallo dove può portare alla confusione nel codice perché un cavallo non lo fa ' concettualmente hanno delle corna. Inoltre, se si eseguono calcoli matematici, inclusi i valori predefiniti, è possibile distorcere i risultati (vedere i commenti nella domanda originale)
moarboilerplate

I mixin, tuttavia, a meno che non vengano eseguiti, sono runtime, sono solo eredità con un altro nome. Il tuo commento "un cavallo non ha concettualmente delle corna" si riferisce al mio commento sulla necessità di saperne di più su ciò che sono, se la nostra risposta deve includere il modo in cui modelliamo cavalli e unicorni e qual è il loro rapporto reciproco. Qualsiasi soluzione che include valori predefiniti è fuori mano imo sbagliato.
Giona

Hai ragione nel dire che per ottenere una soluzione precisa per una specifica manifestazione di questo problema devi avere un sacco di contesto. Per rispondere alla tua domanda su un cavallo con un corno e ricollegarlo ai mixin, stavo pensando a uno scenario in cui una lunghezza di corno mescolata a un cavallo che non è un unicorno è un errore. Considera un tratto Scala che ha un'implementazione predefinita per hornLength che genera un'eccezione. Un tipo di unicorno può sovrascrivere tale implementazione e se un cavallo riesce a trasformarlo in un contesto in cui viene valutata hornLength, è un'eccezione.
Moarboilerplate

-2

Un metodo GetUnicorns () che restituisce un IEnumerable mi sembra la soluzione più elegante, flessibile e universale. In questo modo potresti trattare qualsiasi (combinazione di) tratti che determina se un cavallo passerà come un unicorno, non solo il tipo di classe o il valore di una particolare proprietà.


Sono d'accordo con questo. Mason Wheeler ha anche una buona soluzione nella sua risposta, ma se devi individuare unicorni per molte ragioni diverse in luoghi diversi, il tuo codice avrà molti horses.ofType<Unicorn>...costrutti. Avere una GetUnicornsfunzione sarebbe un liner, ma sarebbe ulteriormente resistente ai cambiamenti nella relazione cavallo / unicorno dal punto di vista del chiamante.
Shaz,

@Ryan Se restituisci un IEnumerable<Horse>, anche se i tuoi criteri di unicorno sono in un posto, è incapsulato, quindi i tuoi chiamanti devono fare ipotesi sul perché hanno bisogno di unicorni (posso ottenere una zuppa di vongole ordinando la zuppa del giorno oggi, ma non lo fa voglio dire lo avrò domani facendo la stessa cosa). Inoltre, devi esporre un valore predefinito per un clacson sul Horse. Se Unicornè di tipo proprio, devi creare un nuovo tipo e mantenere i mapping dei tipi, che possono introdurre un sovraccarico.
moarboilerplate

1
@moarboilerplate: consideriamo tutto ciò che supporta la soluzione. La parte bella è che è indipendente da qualsiasi dettaglio di implementazione dell'unicorno. Sia che tu discrimini in base a un membro dei dati, a una classe o all'ora del giorno (quei cavalli possono trasformarsi tutti in unicorni a mezzanotte se la luna è giusta per tutto quello che so), la soluzione è valida, l'interfaccia rimane la stessa.
Martin Maat,
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.