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 Horse
e un Unicorn
tipo. 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 Circle
sia 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
Ellipse
e, se descrive un cerchio, restituisce il corrispondente Circle
.
Quindi, in termini di programmazione funzionale, il tuo Unicorn
tipo 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 toUnicorn
ha bisogno di essere un'inversa destra di toHorse
:
toUnicorn (toHorse x) = Just x
Il Maybe
tipo di Haskell è ciò che altre lingue chiamano un tipo di "opzione". Ad esempio, il Optional<Unicorn>
tipo Java 8 è o Unicorn
o 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
Horse
tipo;
- Il
Horse
tipo deve codificare informazioni sufficienti per determinare in modo inequivocabile se un valore descrive un unicorno;
- Alcune operazioni del
Horse
tipo devono esporre tali informazioni in modo che i client del tipo possano osservare se un dato Horse
è un unicorno;
- I client del
Horse
tipo dovranno utilizzare queste ultime operazioni in fase di esecuzione per discriminare tra unicorni e cavalli.
Quindi questo è fondamentalmente un modello "chiedi a tutti Horse
se si tratta di un unicorno". Sei diffidente nei confronti di quel modello, ma penso che sia sbagliato. Se ti do un elenco di Horse
s, 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
Horse
tipo;
- Avere
Unicorn
come 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
Horse
un'istanza che descrive un unicorno ma non è Unicorn
un'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 Horse
se si tratta di Unicorn
un'istanza non è sinonimo di chiedere a Horse
se si tratta di un unicorno (se si tratta di una Horse
descrizione 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 Horses
modo tale che ogni volta che un client tenta di costruire un Horse
che descrive un unicorno, la Unicorn
classe 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 Horse
s a Unicorn
s. Questo potrebbe essere un metodo del Horse
tipo:
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 Unicorn
operazione dall'alto, lo stai solo impacchettando in diversi modi (che sicuramente avrà effetti a catena su quali operazioni il Horse
tipo ha bisogno per esporre ai propri clienti).