Biorąc pod uwagę stado koni, jak znaleźć średnią długość rogu wszystkich jednorożców?

30

Powyższe pytanie jest abstrakcyjnym przykładem typowego problemu, który napotykam w starszym kodzie, a ściślej - problemów wynikających z poprzednich prób rozwiązania tego problemu.

Mogę wymyślić co najmniej jedną metodę platformy .NET, która ma rozwiązać ten problem, podobnie jak Enumerable.OfType<T>metoda. Ale fakt, że ostatecznie kończysz przesłuchiwanie typu obiektu w czasie wykonywania, nie pasuje do mnie.

Poza pytaniem każdego konia „Czy jesteś jednorożcem?” przychodzą mi na myśl następujące podejścia:

  • Zgłaszaj wyjątek, gdy podejmowana jest próba uzyskania długości rogu jednorożca (ujawnia funkcjonalność nieodpowiednią dla każdego konia)
  • Zwraca wartość domyślną lub magiczną dla długości rogu jednorożca (wymaga domyślnych kontroli przeszywanych przez dowolny kod, który chce zniszczyć statystyki rogu dla grupy koni, które mogą być wszystkie inne niż jednorożce)
  • Pozbądź się dziedziczenia i stwórz na koniu oddzielny obiekt, który powie ci, czy koń jest jednorożcem, czy nie (co potencjalnie spycha ten sam problem w dół warstwy)

Mam przeczucie, że najlepiej na to odpowiedzieć „brak odpowiedzi”. Ale jak podchodzisz do tego problemu i jeśli to zależy, jaki jest kontekst Twojej decyzji?

Byłbym także zainteresowany wszelkimi spostrzeżeniami na temat tego, czy problem ten nadal występuje w kodzie funkcjonalnym (a może tylko w językach funkcjonalnych, które obsługują zmienność?)

Oznaczono to jako możliwe powtórzenie następującego pytania: Jak uniknąć downcastingu?

Odpowiedź na to pytanie zakłada, że ​​ktoś posiada narzędzie, za HornMeasurerpomocą którego należy wykonać wszystkie pomiary klaksonu. Jest to jednak narzucenie bazy kodu, która powstała na podstawie egalitarnej zasady, że każdy powinien mieć swobodę mierzenia rogu konia.

W przypadku braku a HornMeasurerpodejście przyjęte w odpowiedzi odzwierciedla podejście oparte na wyjątkach wymienione powyżej.

W komentarzach pojawiły się także pewne nieporozumienia dotyczące tego, czy konie i jednorożce są koniowatymi, czy też jednorożec jest magicznym podgatunkiem konia. Należy rozważyć obie możliwości - być może jedna jest lepsza od drugiej?

moarboilerplate
źródło
22
Konie nie mają rogów, więc średnia jest niezdefiniowana (0/0).
Scott Whitlock,
3
@moarboilerplate Anywhere od 10 do nieskończoności.
niania
4
@StephenP: To nie działałoby matematycznie w tym przypadku; wszystkie te 0 zerowałyby średnią.
Mason Wheeler,
3
Jeśli najlepiej odpowiedzieć na pytanie, na które nie ma odpowiedzi, to nie należy ono do witryny pytań i odpowiedzi; Reddit, quora lub inne witryny oparte na dyskusjach są budowane z myślą o rzeczach, w których nie ma odpowiedzi ... powiedziałem, myślę, że może być wyraźnie odpowiedzialna, jeśli szukasz kodu @MasonWheeler podanego, jeśli nie, myślę, że nie mam pojęcia o co próbujesz zapytać ..
Jimmy Hoffa
3
@ JimmyHoffa „robisz to źle” okazuje się akceptowalnym „brakiem odpowiedzi” i często lepszym rozwiązaniem niż „no cóż, oto jeden ze sposobów, w jaki możesz to zrobić” - nie jest wymagana długa dyskusja.
moarboilerplate

Odpowiedzi:

11

Zakładając, że chcesz leczyć Unicorn jako specjalny rodzaj Horse, istnieją zasadniczo dwa sposoby, aby go wymodelować. Bardziej tradycyjny sposób to relacja podklasy. Możesz uniknąć sprawdzania typu i downcastingu, po prostu zmieniając kod, aby listy zawsze były osobne w kontekstach, w których ma to znaczenie, i łącz je tylko w kontekstach, w których nigdy nie dbasz o Unicorncechy. Innymi słowy, ustawiasz go tak, abyś nigdy nie znalazł się w sytuacji, w której musisz wyciągnąć jednorożce ze stada koni. Z początku wydaje się to trudne, ale jest możliwe w 99,99% przypadków i zwykle sprawia, że ​​kod jest znacznie czystszy.

Innym sposobem na wymodelowanie jednorożca jest po prostu podanie wszystkim koniom opcjonalnej długości rogu. Następnie możesz sprawdzić, czy jest to jednorożec, sprawdzając, czy ma długość rogu, i znaleźć średnią długość rogu wszystkich jednorożców według (w Scali):

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

Ta metoda ma tę zaletę, że jest prostsza, z jedną klasą, ale ma tę wadę, że jest znacznie mniej rozszerzalna i ma w pewnym sensie okrągły sposób sprawdzania „jednorożca”. Sztuczka, jeśli zdecydujesz się na to rozwiązanie, polega na rozpoznaniu, kiedy zaczynasz często go rozszerzać, że musisz przejść do bardziej elastycznej architektury. Tego rodzaju rozwiązanie jest znacznie bardziej popularne w językach funkcjonalnych, w których masz proste i wydajne funkcje, takie jak flatMapłatwe odfiltrowywanie Noneelementów.

Karl Bielefeldt
źródło
7
Oczywiście zakłada to, że jedyną różnicą między zwykłym koniem a jednorożcem jest róg. Jeśli tak nie jest, sprawy bardzo szybko się komplikują.
Mason Wheeler
@MasonWheeler tylko w drugiej przedstawionej metodzie.
moarboilerplate
1
Wypatruj komentarzy na temat tego, w jaki sposób jednorożce i jednorożce nigdy nie powinny być łączone razem w scenariuszu dziedziczenia, dopóki nie znajdziesz się w kontekście, w którym nie obchodzi cię jednorożec. Oczywiście, .OfType () może rozwiązać problem i sprawić, że wszystko zadziała, ale rozwiązuje problem, który w ogóle nie powinien istnieć. Jeśli chodzi o drugie podejście, działa, ponieważ opcje są znacznie lepsze niż poleganie na wartości null w celu sugerowania czegoś. Myślę, że drugie podejście można osiągnąć w OO z kompromisem, jeśli hermetycznie ujednolicisz cechy jednorożca i będziesz bardzo czujny.
moarboilerplate
1
pójść na kompromis, jeśli unicestwisz cechy jednorożca w osobnej posesji i będziesz wyjątkowo czujny - po co utrudniać sobie życie. Użyj typeof bezpośrednio i zaoszczędź mnóstwo przyszłych problemów.
gbjbaanb
@gbjbaanb Uważam, że to podejście jest naprawdę odpowiednie tylko w scenariuszach, w których anemika Horsema IsUnicornwłaściwość i pewną UnicornStuffwłaściwość z długością klaksonu (podczas skalowania dla jeźdźca / brokatu wspomnianego w pytaniu).
moarboilerplate 11.01.16
38

Masz prawie wszystkie opcje. Jeśli masz zachowanie zależne od określonego podtypu i jest ono mieszane z innymi typami, Twój kod musi być świadomy tego podtypu; to proste logiczne rozumowanie.

Osobiście po prostu bym poszedł horses.OfType<Unicorn>().Average(u => u.HornLength). Bardzo wyraźnie wyraża intencję kodu, co często jest najważniejsze, ponieważ w końcu ktoś będzie musiał go później utrzymywać.

Mason Wheeler
źródło
Proszę wybacz mi, jeśli moja składnia lambda jest nieprawidłowa; Nie jestem zbytnio programistą w języku C # i nigdy nie potrafię zachować prostych tajemniczych szczegółów. Jednak powinno być jasne, co mam na myśli.
Mason Wheeler,
1
Nie martw się, problem został rozwiązany, gdy lista Unicorni tak zawiera tylko s (dla rekordu, który można pominąć return).
moarboilerplate
4
Oto odpowiedź, którą wybrałbym, gdybym chciał szybko rozwiązać problem. Ale nie odpowiedź, jeśli chciałbym zmienić kod na bardziej wiarygodny.
Andy
6
To zdecydowanie odpowiedź, chyba że potrzebujesz absurdalnego poziomu optymalizacji. Jego przejrzystość i czytelność sprawia, że ​​prawie wszystko inne jest dyskusyjne.
David mówi Przywróć Monikę
1
@DavidGrinberg co zrobić, jeśli napisanie tej czystej, czytelnej metody oznaczałoby, że najpierw musiałbyś zaimplementować strukturę dziedziczenia, która wcześniej nie istniała?
moarboilerplate
9

W .NET nie ma nic złego z:

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

Używanie ekwiwalentu Linq również jest w porządku:

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

W oparciu o pytanie, które zadałeś w tytule, to jest kod, którego spodziewałbym się znaleźć. Gdyby pytanie dotyczyło czegoś takiego jak „średnia zwierząt z rogami”, byłoby inaczej:

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

Zauważ, że podczas korzystania z Linq, Average(i Mini Max) zgłoszą wyjątek, jeśli wyliczalny jest pusty, a typ T nie ma wartości zerowej. Jest tak, ponieważ średnia jest naprawdę niezdefiniowana (0/0). Tak naprawdę potrzebujesz czegoś takiego:

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...
}

Edytować

Myślę, że to wymaga dodania ... jednym z powodów, dla których takie pytanie nie pasuje do programistów zorientowanych obiektowo, jest założenie, że używamy klas i obiektów do modelowania struktury danych. Oryginalna koncepcja obiektowo zorientowana na Smalltalk polegała na zbudowaniu programu z modułów, które były tworzone jako obiekty i wykonywały dla ciebie usługi, gdy wysłałeś im wiadomość. Fakt, że możemy również używać klas i obiektów do modelowania struktury danych, jest (użytecznym) efektem ubocznym, ale są to dwie różne rzeczy. Nawet nie sądzę, że ten ostatni program powinien być uważany za programowanie obiektowe, ponieważ można zrobić to samo zstruct , ale po prostu nie byłoby tak ładnie.

Jeśli używasz programowania obiektowego do tworzenia usług, które robią dla ciebie rzeczy, to z dobrych powodów na ogół nie odpowiada się na pytanie, czy ta usługa jest w rzeczywistości inną usługą lub konkretną implementacją. Otrzymałeś interfejs (zazwyczaj poprzez wstrzyknięcie zależności) i powinieneś zakodować ten interfejs / umowę.

Z drugiej strony, jeśli (błędnie) używasz pomysłów klasy / obiektu / interfejsu do stworzenia struktury danych lub modelu danych, to osobiście nie widzę problemu z wykorzystaniem tego, co jest w pełni. Jeśli zdefiniowałeś, że jednorożce są podtypem koni i ma to sens w twojej domenie, to koniecznie przeprowadź zapytanie o konie w stadzie, aby znaleźć jednorożce. W końcu w takim przypadku zwykle staramy się stworzyć język specyficzny dla domeny, aby lepiej wyrazić rozwiązania problemów, które musimy rozwiązać. W tym sensie nie ma nic złego w .OfType<Unicorn>()itp.

Ostatecznie pobranie kolekcji elementów i przefiltrowanie ich według typu jest tak naprawdę tylko programowaniem funkcjonalnym, a nie programowaniem obiektowym. Na szczęście języki takie jak C # są teraz wygodne w obsłudze obu paradygmatów.

Scott Whitlock
źródło
7
Wiesz już, że animal to Unicorn ; po prostu rzuć zamiast używać aslub potencjalnie jeszcze lepiej użyj, as a następnie sprawdź, czy nie ma wartości null.
Philip Kendall
3

Ale fakt, że ostatecznie kończysz przesłuchiwanie typu obiektu w czasie wykonywania, nie pasuje do mnie.

Problem z tym stwierdzeniem polega na tym, że bez względu na to, jakiego mechanizmu używasz, zawsze będziesz przesłuchiwał obiekt, aby stwierdzić, jaki to typ. Może to być RTTI lub może to być unia lub zwykła struktura danych, o którą pytaszif horn > 0 . Dokładne szczegóły zmieniają się nieznacznie, ale cel jest taki sam - pytasz obiekt o siebie w jakiś sposób, aby sprawdzić, czy powinieneś go dalej przesłuchiwać.

Biorąc to pod uwagę, warto w tym celu skorzystać ze wsparcia języka. W .NET, którego byś używałtypeof .

Powodem tego jest nie tylko dobre posługiwanie się językiem. Jeśli masz obiekt, który wygląda jak inny, ale z pewną niewielką zmianą, istnieje szansa, że ​​z czasem znajdziesz więcej różnic. W twoim przykładzie jednorożców / koni możesz powiedzieć, że jest tylko długość rogu ... ale jutro sprawdzisz, czy potencjalny jeździec jest dziewicą, czy też kupa jest błyszcząca. (klasyczny przykład w świecie rzeczywistym to widżety GUI, które wywodzą się ze wspólnej bazy i trzeba inaczej szukać pól wyboru i list boxów. Liczba różnic byłaby zbyt duża, aby po prostu stworzyć pojedynczy super obiekt, który posiadałby wszystkie możliwe kombinacje danych ).

Jeśli sprawdzanie typu obiektu w czasie wykonywania nie działa dobrze, wtedy alternatywą jest podzielenie różnych obiektów od samego początku - zamiast przechowywać jedno stado jednorożców / koni, masz 2 kolekcje - jedną dla koni, drugą dla jednorożców . Może to działać bardzo dobrze, nawet jeśli przechowujesz je w specjalnym kontenerze (np. Multimapa, w której kluczem jest typ obiektu ... ale nawet mimo tego, że przechowujemy je w 2 grupach, natychmiast wracamy do badania typu obiektu !)

Z pewnością podejście oparte na wyjątkach jest błędne. Używanie wyjątków jako normalnego przepływu programu to zapach kodu (jeśli miałeś stado jednorożców i osła z muszelką przyklejoną do głowy, to powiedziałbym, że podejście oparte na wyjątkach jest OK, ale jeśli masz stado jednorożców a konie sprawdzające każdego pod kątem jednorożca nie są nieoczekiwane. Wyjątek stanowią wyjątkowe okoliczności, a nie skomplikowane ifstwierdzenie). W każdym razie użycie wyjątków dla tego problemu polega po prostu na zapytaniu o typ obiektu w czasie wykonywania, tylko tutaj niewłaściwie używasz funkcji języka do sprawdzania obiektów innych niż jednorożec. Równie dobrze możesz napisać wif horn > 0 i przynajmniej przetwarzają Twoją kolekcję szybko, wyraźnie, używając mniejszej liczby wierszy kodu i unikając problemów pojawiających się po zgłoszeniu innych wyjątków (np. pusta kolekcja lub próba zmierzenia muszli tego osła)

gbjbaanb
źródło
W kontekście starszym if horn > 0jest to sposób, w jaki problem ten jest rozwiązany na początku. Następnie problemy, które zwykle pojawiają się, gdy chcesz sprawdzić jeźdźców i brokat, i horn > 0są zakopane w całym miejscu w niepowiązanym kodzie (również kod cierpi na tajemnicze błędy z powodu braku kontroli, gdy klakson ma wartość 0). Poza tym podklasowanie konia po fakcie jest zwykle najdroższą propozycją, więc zazwyczaj nie jestem skłonny do tego, jeśli nadal są skupieni na końcu reaktora. Tak więc z pewnością stało się „jak brzydkie są alternatywy”
moarboilerplate
@moarboilerplate sam to powiesz, skorzystaj z taniego i łatwego rozwiązania, a zamieni się w bałagan. Dlatego wymyślono języki OO jako rozwiązanie tego rodzaju problemu. podklasowanie konia może początkowo wydawać się drogie, ale wkrótce się zwróci. Kontynuacja prostego, ale mętnego rozwiązania kosztuje z czasem coraz więcej.
gbjbaanb
3

Ponieważ pytanie ma functional-programmingznacznik, możemy użyć typu sumy, aby odzwierciedlić dwa smaki koni i dopasować wzór, aby jednoznacznie między nimi. Na przykład w 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

W porównaniu z OOP, FP ma tę zaletę, że separuje dane / funkcje, co może uchronić cię przed (nieuzasadnionym?) Poczuciem winy polegającym na naruszeniu poziomu abstrakcji podczas sprowadzania do określonych podtypów z listy obiektów nadtypu.

W przeciwieństwie do rozwiązań OO zaproponowanych w innych odpowiedziach, dopasowywanie wzorców zapewnia również łatwiejszy punkt rozszerzenia, jeśli inny gatunek Rogaty Equinepojawi się pewnego dnia.

guillaume31
źródło
2

Krótka forma tej samej odpowiedzi na końcu wymaga przeczytania książki lub artykułu internetowego.

Wzór gościa

Problem dotyczy mieszanki koni i jednorożców. (Naruszenie zasady podstawienia Liskowa jest częstym problemem w starszych bazach kodowych.)

Dodaj metodę do konia i wszystkich podklas

Horse.visit(EquineVisitor v)

Interfejs gościa koni wygląda mniej więcej tak w 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);
}

Aby zmierzyć rogi, piszemy teraz ....

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;
}

Wzorzec odwiedzających jest krytykowany za utrudnienie refaktoryzacji i wzrostu.

Krótka odpowiedź: użyj wzorca projektu Visitor, aby uzyskać podwójną wysyłkę.

patrz także https://en.wikipedia.org/wiki/Visitor_pattern

zobacz także http://c2.com/cgi/wiki?VisitorPattern w celu omówienia odwiedzających.

patrz także Design Patterns autorstwa Gamma i in.

Tim Williscroft
źródło
Już miałem sam odpowiedzieć na wzór gości. Musiałem przewinąć w dół zaskakujący sposób, aby sprawdzić, czy ktoś już o tym wspominał!
Ben Thurley,
0

Zakładając, że w twojej architekturze jednorożce są podgatunkiem konia i napotykasz miejsca, w których możesz znaleźć kolekcję miejsc, w Horsektórych niektóre z nich mogą być Unicorn, osobiście wybrałbym pierwszą metodę ( .OfType<Unicorn>()...), ponieważ jest to najprostszy sposób wyrażenia twojego zamiaru . Dla każdego, kto przyjdzie później (w tym ciebie za 3 miesiące), od razu jest oczywiste, co próbujesz osiągnąć za pomocą tego kodu: wybierz jednorożce spośród koni.

Inne wymienione metody wydają się być kolejnym sposobem zadawania pytania „Czy jesteś jednorożcem?”. Na przykład, jeśli używasz metody pomiaru rogów opartej na wyjątkach, możesz mieć kod, który wyglądałby mniej więcej tak:

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

Tak więc teraz wyjątek staje się wskaźnikiem, że coś nie jest jednorożcem. A teraz nie jest to tak naprawdę wyjątkowa sytuacja, ale jest częścią normalnego przebiegu programu. A użycie wyjątku zamiast ifwydaje się jeszcze bardziej brudne niż samo sprawdzanie typu.

Powiedzmy, że podążasz magiczną drogą sprawdzania rogów koni. Więc teraz twoje klasy wyglądają mniej więcej tak:

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

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

Teraz twoja Horseklasa musi wiedzieć o Unicornklasie i mieć dodatkowe metody radzenia sobie z rzeczami, na których jej nie zależy. Teraz wyobraź sobie, że masz także Pegasuss i Zebras, które dziedziczą z Horse. Teraz Horsepotrzebuje Flysposobu jak MeasureWings, CountStripesitd A potem Unicornklasa dostaje zbyt tych metod. Teraz wszystkie twoje klasy muszą się o sobie znać i zatrułeś je kilkoma metodami, których nie powinno być po prostu po to, by uniknąć pytania typu „Czy to jednorożec?”

A co z dodaniem czegoś do Horses, aby powiedzieć, czy coś jest Unicorni poradzi sobie z pomiarem wszystkich rogów? Cóż, teraz musisz sprawdzić istnienie tego obiektu, aby wiedzieć, czy coś jest jednorożcem (co tylko zastępuje jeden czek na inny). To także trochę zabrudza wody w tym, że teraz możesz miećList<Horse> unicornsto naprawdę zawiera wszystkie jednorożce, ale system typów i debugger nie mogą tego łatwo powiedzieć. „Ale wiem, że to wszystkie jednorożce”, mówicie, „nazwa nawet tak mówi”. Co jeśli coś było źle nazwane? Albo powiedzmy, że napisałeś coś z założeniem, że tak naprawdę będą to wszystkie jednorożce, ale potem zmieniły się wymagania i teraz może mieć również pomieszane pegazy? (Ponieważ nic takiego nigdy się nie zdarza, szczególnie w starszych programach / sarkazmie.) Teraz system pisma z przyjemnością wprowadzi twoje pegasi do twoich jednorożców. Jeśli twoja zmienna została zadeklarowana jako List<Unicorn>kompilator (lub środowisko wykonawcze), pasowałaby, gdybyś próbował mieszać pegasi lub konie.

Wreszcie, wszystkie te metody są tylko zamiennikiem kontroli systemu typów. Osobiście wolałbym nie wymyślać tu na nowo koła i mam nadzieję, że mój kod działa tak samo dobrze, jak coś wbudowanego i przetestowanego przez tysiące innych programistów tysiące razy.

Ostatecznie kod musi być dla Ciebie zrozumiały . Komputer to rozwiąże, niezależnie od tego, jak go napiszesz. To ty musisz go debugować i mieć powód do tego. Dokonaj wyboru, który ułatwi Ci pracę. Jeśli z jakiegoś powodu jedna z tych innych metod oferuje przewagę, która przewyższa wyraźniejszy kod w kilku miejscach, które by się pojawiły, idź. Ale to zależy od twojej bazy kodu.

Becuzz
źródło
Cichy wyjątek jest zdecydowanie zły - moja propozycja była sprawdzeniem, if(horse.IsUnicorn) horse.MeasureHorn();a wyjątki nie zostałyby wyłapane - albo zostałyby uruchomione, gdy !horse.IsUnicornjesteś w kontekście pomiaru jednorożca, albo wewnątrz MeasureHornnie-jednorożca. W ten sposób, gdy zostanie zgłoszony wyjątek, nie maskujesz błędów, całkowicie wysadza się w powietrze i jest znakiem, że coś trzeba naprawić. Oczywiście jest to odpowiednie tylko w niektórych scenariuszach, ale jest to implementacja, która nie używa zgłaszania wyjątków w celu ustalenia ścieżki wykonania.
moarboilerplate
0

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:

  1. Wartości tego typu są odpowiednio reprezentacjami lub opisami koni i jednorożców;
  2. 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:

  1. 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;
  2. 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ć:

  1. Twój model musi mieć Horse typ;
  2. The Horse potrzeby Typ zakodować wystarczająco dużo informacji, aby określić jednoznacznie czy jakakolwiek wartość opisuje jednorożca;
  3. Niektóre operacje Horse typu muszą ujawniać te informacje, aby klienci tego typu mogli zaobserwować, czy dany Horsejednorożec jest dany;
  4. 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).

sacundim
źródło
-1

Pomyślałem, że komentarz OP w innej odpowiedzi wyjaśniał pytanie

to część tego, o co pyta też pytanie. Jeśli mam stado koni, a niektóre z nich są koncepcyjnie jednorożcami, to jak powinny istnieć, aby problem można było rozwiązać bez zbyt wielu negatywnych skutków?

Tak sformułowane, myślę, że potrzebujemy więcej informacji. Odpowiedź prawdopodobnie zależy od wielu rzeczy:

  • Nasze udogodnienia językowe. Np. Prawdopodobnie podchodziłbym do tego inaczej w ruby, javascript i Javie.
  • Same koncepcje: Jaki jest koń, a co jest jednorożca? Jakie dane są z nimi powiązane? Czy są dokładnie takie same, z wyjątkiem klaksonu, czy też mają inne różnice?
  • Jak inaczej ich używamy, poza uśrednianiem wartości długości rogu? A co ze stadami? Może też powinniśmy je modelować? Czy używamy ich gdzie indziej? herd.averageHornLength()wydaje się pasować do naszego modelu koncepcyjnego.
  • Jak powstają obiekty konia i jednorożca? Czy zmiana tego kodu mieści się w granicach naszego refaktoryzacji?

Ogólnie jednak nie pomyślałbym nawet o dziedziczeniu i podtypach tutaj. Masz listę obiektów. Niektóre z tych obiektów można zidentyfikować jako jednorożce, być może dlatego, że mają hornLength()metodę. Filtruj listę na podstawie tej właściwości unikalnej dla jednorożca. Teraz problem został zredukowany do uśredniania długości klaksonu na liście jednorożców.

OP, daj mi znać, jeśli nadal będę nieporozumienie ...

Jonasz
źródło
1
Uczciwe punkty. Aby problem nie stał się jeszcze bardziej abstrakcyjny, musimy przyjąć pewne rozsądne założenia: 1) język silnie typowany 2) stado ogranicza konie do jednego typu, prawdopodobnie ze względu na zbiór 3) technik takich jak pisanie kaczek prawdopodobnie należy unikać . Co do tego, co można zmienić, niekoniecznie istnieją ograniczenia, ale każdy rodzaj zmiany ma swoje własne unikalne konsekwencje ...
moarboilerplate
Jeśli stado ogranicza konie do jednego typu, nie jest to nasze jedyne dziedziczenie (nie podoba mi się ta opcja) lub obiekt otulający (powiedzmy HerdMember), który inicjujemy z koniem lub jednorożcem (uwalniając konia i jednorożca od potrzeby relacji podtypu ). HerdMembermoże następnie wdrożyć dowolnie, isUnicorn()ale uzna to za stosowne, a następnie sugeruję rozwiązanie filtrujące.
Jonasz
W niektórych językach można łączyć hornLength (), a jeśli tak, może to być prawidłowe rozwiązanie. Jednak w językach, w których pisanie jest mniej elastyczne, musisz zastosować pewne hackerskie techniki, aby zrobić to samo, lub musisz zrobić coś takiego, jak nałożenie końcówki horn na konia, gdzie może to prowadzić do pomyłek w kodzie, ponieważ koń nie „ t koncepcyjnie mają rogi. Ponadto, jeśli obliczenia matematyczne, w tym wartości domyślne, mogą wypaczać wyniki (patrz komentarze pod oryginalnym pytaniem)
moarboilerplate
Jednak miksy, chyba że są gotowe, są środowiskiem wykonawczym, są po prostu dziedziczeniem pod inną nazwą. Twój komentarz „koń nie ma koncepcyjnie żadnych rogów” odnosi się do mojego komentarza na temat potrzeby dowiedzenia się więcej o tym, czym one są, jeśli nasza odpowiedź musi obejmować sposób modelowania koni i jednorożców oraz ich związek ze sobą. Każde rozwiązanie, które zawiera wartości domyślne, jest pod ręką niewłaściwe imo.
Jonasz
Masz rację, aby uzyskać dokładne rozwiązanie konkretnego przejawu tego problemu, musisz mieć dużo kontekstu. Aby odpowiedzieć na twoje pytanie dotyczące konia z rogiem i związać go z mixinami, myślałem o scenariuszu, w którym róg o długości zmieszanej z koniem, który nie jest jednorożcem, jest błędem. Rozważ cechę Scala, która ma domyślną implementację dla hornLength, która zgłasza wyjątek. Typ jednorożca może zastąpić tę implementację, a jeśli koń kiedykolwiek stworzy kontekst, w którym oceniana jest wartość hornLength, jest to wyjątek.
moarboilerplate
-2

Metoda GetUnicorns (), która zwraca IEnumerable, wydaje mi się najbardziej eleganckim, elastycznym i uniwersalnym rozwiązaniem. W ten sposób możesz poradzić sobie z dowolną (kombinacją) cech, które określają, czy koń przejdzie jako jednorożec, a nie tylko typ klasy lub wartość określonej właściwości.

Martin Maat
źródło
Zgadzam się z tym. Mason Wheeler ma również dobre rozwiązanie w swojej odpowiedzi, ale jeśli chcesz wyróżnić jednorożce z wielu różnych powodów w różnych miejscach, twój kod będzie miał wiele horses.ofType<Unicorn>...konstrukcji. Pełniąc GetUnicornsfunkcję, byłby to jeden liniowiec, ale byłby ponadto odporny na zmiany w relacji koń / jednorożec z perspektywy dzwoniącego.
Shaz
@ Ryan Jeśli zwrócisz IEnumerable<Horse>, chociaż twoje kryteria jednorożca są w jednym miejscu, są zamknięte, więc dzwoniący muszą poczynić założenia, dlaczego potrzebują jednorożców (mogę uzyskać zupę z małży, zamawiając dziś zupę dnia, ale to nie oznacza to znaczy, że dostanę to jutro, robiąc to samo). Ponadto musisz udostępnić wartość domyślną rogu na Horse. Jeśli Unicornjest to własny typ, musisz utworzyć nowy typ i zachować odwzorowania typów, które mogą wprowadzić narzut.
moarboilerplate
1
@moarboilerplate: Uważamy, że to wszystko wspiera rozwiązanie. Część piękna polega na tym, że jest ona niezależna od jakichkolwiek szczegółów implementacyjnych jednorożca. Bez względu na to, czy dyskryminujesz ze względu na członka danych, klasę czy porę dnia (te konie mogą zmienić się w jednorożce o północy, jeśli księżyc jest odpowiedni dla wszystkich, których znam), rozwiązanie pozostaje, interfejs pozostaje taki sam.
Martin Maat,