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 Horse
i Unicorn
typ. 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 Circle
jest 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
Ellipse
i, jeśli opisuje okrąg, zwraca odpowiednią Circle
.
Tak więc, pod względem programowania funkcjonalnego, twój Unicorn
typ 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 Maybe
Typ jest tym, co inne języki nazywają typem „opcji”. Na przykład Optional<Unicorn>
typ Java 8 jest albo „ Unicorn
nic”, 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 Horse
jednorożec jest dany;
- Klienci tego
Horse
typu 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ę Horse
s, 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
Unicorn
jako podtypHorse
;
- Użyj refleksji typu środowiska wykonawczego jako operacji dostępnej dla klienta, która rozpoznaje, czy dana
Horse
jest 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
Horse
instancję, która opisuje jednorożca, ale nie jest Unicorn
instancją?
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, Horse
czy to Unicorn
instancja, nie jest równoznaczne z pytaniem, Horse
czy jest to jednorożec (czy jest to Horse
opis 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, Horses
tak że za każdym razem, gdy klient próbuje zbudować Horse
opisujący jednorożca, Unicorn
tworzona 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 Horse
s na Unicorn
s. Może to być metoda Horse
typu:
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 Unicorn
operacji z góry, po prostu pakujesz go na różne sposoby (co wprawdzie będzie miało falowy wpływ na operacje, których Horse
potrzebuje ten typ ujawniać swoim klientom).