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:
- I valori di questi tipi sono rappresentazioni o descrizioni di cavalli e unicorni (rispettivamente);
- 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:
- C'è una funzione totale che converte qualsiasi
Circle(descrizione del cerchio schematizzata) in un Ellipse(diverso tipo di descrizione) che descrive gli stessi cerchi;
- 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 è:
- Il tuo modello deve avere un
Horsetipo;
- Il
Horsetipo deve codificare informazioni sufficienti per determinare in modo inequivocabile se un valore descrive un unicorno;
- Alcune operazioni del
Horsetipo devono esporre tali informazioni in modo che i client del tipo possano osservare se un dato Horseè un unicorno;
- 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).