Cóż, wygląda na to, że twoja domena semantyczna ma relację IS-A, ale jesteś nieco ostrożny w używaniu podtypów / dziedziczenia do modelowania tego - szczególnie ze względu na odbicie typu środowiska wykonawczego. Myślę jednak, że boisz się niewłaściwej rzeczy - podsieci rzeczywiście wiążą się z niebezpieczeństwami, ale fakt, że pytasz o obiekt w czasie wykonywania, nie stanowi problemu. Zobaczysz o co mi chodzi.
Programowanie obiektowe dość mocno opierało się na pojęciu relacji IS-A, prawdopodobnie opierało się na nim zbyt mocno, prowadząc do dwóch znanych koncepcji krytycznych:
Myślę jednak, że istnieje inny, bardziej oparty na programowaniu funkcjonalnym sposób spojrzenia na relacje IS-A, który być może nie ma takich trudności. Po pierwsze, chcemy modelować konie i jednorożce w naszym programie, więc będziemy mieć typ Horsei Unicorntyp. Jakie są wartości tego typu? Powiedziałbym to:
- Wartości tego typu są odpowiednio reprezentacjami lub opisami koni i jednorożców;
- Są schematycznymi przedstawieniami lub opisami - nie mają one swobodnej formy, są zbudowane według bardzo surowych zasad.
Może to zabrzmieć oczywisto, ale myślę, że jednym ze sposobów, w jaki ludzie wpadają w takie problemy, jak problem z elipsą koła, jest niedostateczne uważanie na te punkty. Każde koło jest elipsą, ale to nie znaczy, że każdy schematyczny opis koła jest automatycznie schematycznym opisem elipsy według innego schematu. Innymi słowy, to, że okrąg jest elipsą, nie oznacza, że a Circlejest Ellipse, że tak powiem. Ale to oznacza, że:
- Istnieje całkowita funkcja, która przekształca dowolny
Circle(opis schematu okręgu) w Ellipse(inny typ opisu) opisujący te same koła;
- Istnieje funkcja częściowa, która przyjmuje
Ellipsei, jeśli opisuje okrąg, zwraca odpowiednią Circle.
Tak więc, pod względem programowania funkcjonalnego, twój Unicorntyp wcale nie musi być podtypem Horse, potrzebujesz tylko takich operacji:
-- 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
I toUnicorn musi być właściwą odwrotnością toHorse:
toUnicorn (toHorse x) = Just x
Haskella MaybeTyp jest tym, co inne języki nazywają typem „opcji”. Na przykład Optional<Unicorn>typ Java 8 jest albo „ Unicornnic”, albo „niczym”. Zauważ, że dwie z twoich alternatyw - rzucenie wyjątku lub zwrócenie „wartości domyślnej lub magicznej” - są bardzo podobne do typów opcji.
Zasadniczo zrekonstruowałem pojęcie relacji IS-A pod względem typów i funkcji, bez użycia podtypów i dziedziczenia. Chciałbym od tego zabrać:
- Twój model musi mieć
Horse typ;
- The
Horse potrzeby Typ zakodować wystarczająco dużo informacji, aby określić jednoznacznie czy jakakolwiek wartość opisuje jednorożca;
- Niektóre operacje
Horse typu muszą ujawniać te informacje, aby klienci tego typu mogli zaobserwować, czy dany Horsejednorożec jest dany;
- Klienci tego
Horsetypu będą musieli wykorzystać te ostatnie operacje w czasie wykonywania, aby rozróżnić jednorożce od koni.
Jest to więc „zapytaj każdego” Horse model czy to jednorożec”. Obawiasz się tego modelu, ale nie sądzę. Jeśli dam ci listę Horses, wszystko, co gwarantuje ten typ, to to, że elementy, które opisują na liście, to konie - więc nieuchronnie będziesz musiał zrobić coś w czasie wykonywania, aby powiedzieć, które z nich są jednorożcami. Myślę, że nie da się tego obejść - musisz wdrożyć operacje, które zrobią to za Ciebie.
W programowaniu obiektowym znany sposób to:
- Mieć
Horse typ;
- Podaj
Unicornjako podtypHorse ;
- Użyj refleksji typu środowiska wykonawczego jako operacji dostępnej dla klienta, która rozpoznaje, czy dana
Horsejest Unicorn.
Ma to dużą słabość, kiedy patrzysz na to z perspektywy „rzecz kontra opis”, którą przedstawiłem powyżej:
- Co jeśli masz
Horseinstancję, która opisuje jednorożca, ale nie jest Unicorninstancją?
Wracając do początku, myślę, że to naprawdę przerażająca część wykorzystywania podtypów i downcastów do modelowania tej relacji IS-A - nie fakt, że musisz wykonać kontrolę czasu wykonywania. Nadużywanie trochę typografii, pytanie, Horseczy to Unicorninstancja, nie jest równoznaczne z pytaniem, Horseczy jest to jednorożec (czy jest to Horseopis konia, który jest również jednorożcem). Nie, chyba że twój program dołożył wszelkich starań, aby obudować kod, który konstruuje, Horsestak że za każdym razem, gdy klient próbuje zbudować Horseopisujący jednorożca, Unicorntworzona jest instancja klasy. Z mojego doświadczenia wynika, że programiści rzadko robią to ostrożnie.
Więc wybrałbym podejście, w którym istnieje jawna, nieprzekraczająca operacja, która konwertuje Horses na Unicorns. Może to być metoda Horsetypu:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... lub może to być obiekt zewnętrzny („oddzielny obiekt na koniu, który mówi, czy koń jest jednorożcem, czy nie”):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
Wybór między nimi zależy od tego, jak zorganizowany jest twój program - w obu przypadkach masz odpowiednik mojej Horse -> Maybe Unicornoperacji z góry, po prostu pakujesz go na różne sposoby (co wprawdzie będzie miało falowy wpływ na operacje, których Horsepotrzebuje ten typ ujawniać swoim klientom).