Sue jest projektowanie biblioteki JavaScript, Magician.js
. Jego zawleczka jest funkcją, która wyciąga Rabbit
z przekazanego argumentu.
Wie, że jego użytkownicy mogą chcieć wyciągnąć królika z String
, a Number
, a Function
może nawet a HTMLElement
. Mając to na uwadze, mogłaby zaprojektować swój interfejs API w następujący sposób:
Surowy interfejs
Magician.pullRabbitOutOfString = function(str) //...
Magician.pullRabbitOutOfHTMLElement = function(htmlEl) //...
Każda funkcja w powyższym przykładzie wiedziałaby, jak obsługiwać argument typu określonego w nazwie funkcji / nazwie parametru.
Lub mogłaby to zaprojektować tak:
Interfejs „ad hoc”
Magician.pullRabbit = function(anything) //...
pullRabbit
musiałby uwzględnić różnorodność różnych oczekiwanych typów, którymi anything
może być argument, a także (oczywiście) nieoczekiwany typ:
Magician.pullRabbit = function(anything) {
if (anything === undefined) {
return new Rabbit(); // out of thin air
} else if (isString(anything)) {
// more
} else if (isNumber(anything)) {
// more
}
// etc.
};
Pierwsza (ścisła) wydaje się bardziej wyraźna, być może bezpieczniejsza i być może bardziej wydajna - ponieważ istnieje niewiele lub nie ma żadnych kosztów ogólnych związanych z sprawdzaniem lub konwersją typów. Ale ten drugi (ad hoc) wydaje się łatwiejszy, patrząc na to z zewnątrz, ponieważ „po prostu działa” z dowolnym argumentem, który konsument API uzna za dogodny do przekazania.
Aby uzyskać odpowiedź na to pytanie , chciałbym zobaczyć konkretne zalety i wady podejścia (lub innego podejścia, jeśli żadne z nich nie jest idealne), które Sue powinna wiedzieć, jakie podejście zastosować przy projektowaniu interfejsu API swojej biblioteki.
źródło
Odpowiedzi:
Niektóre zalety i wady
Plusy dla polimorficznych:
Plusy dla ad-hoc:
Cat
instancji? Czy to po prostu zadziała? jeśli nie, jakie jest zachowanie? Jeśli nie ograniczę tego typu, muszę to zrobić w dokumentacji lub w testach, które mogłyby pogorszyć kontrakt.pullRabbitOutOfString
wersja będzie prawdopodobnie znacznie szybsza w silnikach takich jak V8. Zobacz ten film, aby uzyskać więcej informacji. Edycja: Sam napisałem perf, okazuje się, że w praktyce nie zawsze tak jest .Niektóre alternatywne rozwiązania:
Moim zdaniem ten rodzaj projektu nie jest zbyt „Java-Scripty” na początek. JavaScript to inny język z różnymi idiomami niż języki takie jak C #, Java czy Python. Te idiomy powstały w latach, gdy programiści próbowali zrozumieć słabe i mocne strony języka, staram się trzymać tych idiomów.
Są dwa fajne rozwiązania, o których mogę myśleć:
Rozwiązanie 1: Podnoszenie obiektów
Jednym z powszechnych rozwiązań tego problemu jest „podnoszenie” obiektów z możliwością wyciągnięcia z nich królików.
Oznacza to, że ma funkcję, która bierze jakiś obiekt i dodaje do tego wyciągnięcie kapelusza. Coś jak:
Mogę tworzyć takie
makePullable
funkcje dla innych obiektów, mógłbym stworzyćmakePullableString
itp. Definiuję konwersję dla każdego typu. Jednak po podniesieniu obiektów nie mam typu, aby używać ich w ogólny sposób. Interfejs w JavaScript jest określony przez wpisanie kaczki, jeśli mapullOfHat
metodę, mogę ją pobrać za pomocą metody Magika.Wtedy Magik mógłby zrobić:
Unoszenie obiektów, użycie pewnego rodzaju wzoru mieszania wydaje się być bardziej rzeczą JS do zrobienia. (Zauważ, że jest to problematyczne w przypadku typów wartości w języku, które są łańcuchem, liczbą, wartością null, niezdefiniowaną i wartością logiczną, ale wszystkie mają możliwość wstawiania pól)
Oto przykład, jak może wyglądać taki kod
Rozwiązanie 2: Wzór strategii
Omawiając to pytanie na czacie JS w StackOverflow, mój przyjaciel fenomnomnominal zasugerował użycie wzorca strategii .
Umożliwiłoby to dodanie umiejętności wyciągania królików z różnych obiektów w czasie wykonywania i tworzyłby bardzo kod JavaScript. Mag może nauczyć się wyciągać z kapeluszy przedmioty różnych typów i wyciąga je na podstawie tej wiedzy.
Oto jak może to wyglądać w CoffeeScript:
Odpowiedni kod JS można zobaczyć tutaj .
W ten sposób czerpiesz korzyści z obu światów, akcja jak ciągnąć nie jest ściśle związana ani z przedmiotami, ani z Magiem i myślę, że jest to bardzo miłe rozwiązanie.
Użycie byłoby coś takiego:
źródło
Problem polega na tym, że próbujesz zaimplementować rodzaj polimorfizmu, który nie istnieje w JavaScript - JavaScript jest prawie powszechnie najlepiej traktowany jako język typu kaczego, nawet jeśli obsługuje niektóre typy czcionek.
Aby stworzyć najlepszy interfejs API, odpowiedzią jest, że powinieneś wdrożyć oba. To trochę więcej pisania, ale na dłuższą metę zaoszczędzi dużo pracy użytkownikom interfejsu API.
pullRabbit
powinna być po prostu metodą arbitrażową, która sprawdza typy i wywołuje odpowiednią funkcję związaną z danym typem obiektu (nppullRabbitOutOfHtmlElement
.).W ten sposób użytkownicy prototypów mogą korzystać
pullRabbit
, ale jeśli zauważą spowolnienie, mogą wdrożyć kontrolę typu na swoim końcu (prawdopodobnie szybciej) i po prostu zadzwonićpullRabbitOutOfHtmlElement
bezpośrednio.źródło
To jest JavaScript. Gdy stajesz się lepszy, zauważysz, że często znajduje się środkowa droga, która pomaga negować takie dylematy. Poza tym naprawdę nie ma znaczenia, czy jakiś nieobsługiwany „typ” zostanie złapany przez coś, czy ulegnie awarii, gdy ktoś spróbuje go użyć, ponieważ nie ma kompilacji w czasie wykonywania. Jeśli użyjesz go źle, psuje się. Próba ukrycia, że się zepsuła lub sprawiła, że działała w połowie, kiedy się zepsuła, nie zmienia faktu, że coś jest zepsute.
Więc zjedz swoje ciasto i zjedz je, a także naucz się unikać pomyłek typu i niepotrzebnego łamania, utrzymując wszystko naprawdę, naprawdę oczywiste, jak w dobrze nazwanym i ze wszystkimi właściwymi szczegółami we właściwych miejscach.
Przede wszystkim gorąco zachęcam do przyzwyczajenia się do ustawiania kaczek z rzędu, zanim będzie trzeba sprawdzać typy. Najsmuklejszym i najbardziej wydajnym (ale nie zawsze najlepszym w przypadku rodzimych konstruktorów) jest najpierw uderzenie w prototypy, aby twoja metoda nie musiała nawet przejmować się obsługiwanym typem.
Uwaga: Powszechnie uważa się za złą formę robienia tego z Object, ponieważ wszystko dziedziczy po Object. Osobiście też unikałbym Funkcji. Niektórzy mogą odczuwać zaniepokojenie dotykaniem prototypu dowolnego natywnego konstruktora, co może nie być złą polityką, ale przykład może nadal służyć podczas pracy z własnymi konstruktorami obiektów.
Nie martwiłbym się tym podejściem w przypadku metody o konkretnym zastosowaniu, która prawdopodobnie nie zablokuje czegoś z innej biblioteki w mniej skomplikowanej aplikacji, ale dobrym instynktem jest unikanie twierdzenia, że zbyt wiele metod rodzimych w JavaScript, jeśli nie musisz, chyba że normalizujesz nowsze metody w nieaktualnych przeglądarkach.
Na szczęście zawsze możesz po prostu wstępnie odwzorować typy lub nazwy konstruktorów na metody (strzeż się IE <= 8, który nie ma <object> .constructor.name, wymagając, abyś go przeanalizował z wyników toString z właściwości konstruktora). Wciąż sprawdzasz nazwę konstruktora (typeof jest dość bezużyteczny w JS podczas porównywania obiektów), ale przynajmniej czyta o wiele ładniej niż gigantyczna instrukcja switch lub łańcuch / / else w każdym wywołaniu metody do czegoś, co może być szerokie różnorodność obiektów.
Lub stosując tę samą metodę mapowania, jeśli chcesz uzyskać dostęp do komponentu „ten” różnych typów obiektów, aby korzystać z nich tak, jakby były metodami bez dotykania odziedziczonych prototypów:
Dobrą ogólną zasadą w każdym języku IMO jest próba uporządkowania szczegółów rozgałęzienia, zanim dojdziesz do kodu, który faktycznie pociąga za spust. W ten sposób można łatwo zobaczyć wszystkich graczy zaangażowanych w ten najwyższy poziom API, aby uzyskać przyjemny przegląd, ale także znacznie łatwiej jest ustalić, gdzie można znaleźć szczegóły, które mogą być dla kogoś ważne.
Uwaga: wszystko to nie zostało przetestowane, ponieważ zakładam, że nikt tak naprawdę nie ma do tego zastosowania RL. Jestem pewien, że są literówki / błędy.
źródło
To (dla mnie) jest interesujące i skomplikowane pytanie, na które należy odpowiedzieć. Naprawdę podoba mi się to pytanie, więc postaram się odpowiedzieć. Jeśli w ogóle przeprowadzisz badania dotyczące standardów programowania w javascript, znajdziesz tyle „właściwych” sposobów na zrobienie tego, co ludzie, którzy reklamują „właściwy” sposób.
Ale skoro szukasz opinii, która droga jest lepsza. Tutaj nic nie idzie.
Osobiście wolałbym podejście „adhoc”. Pochodzę z tła c ++ / C #, to bardziej mój styl rozwoju. Możesz utworzyć jedno żądanie pullRabbit i sprawić, aby ten jeden typ żądania sprawdził przekazany argument i zrobił coś. Oznacza to, że nie musisz się martwić, jaki typ argumentu jest przekazywany w dowolnym momencie. Jeśli zastosujesz podejście ścisłe, nadal będziesz musiał sprawdzić, jaki typ jest zmienna, ale zamiast tego zrobiłbyś to przed wywołaniem metody. Tak więc ostatecznie pytanie brzmi: czy chcesz sprawdzić typ przed wykonaniem połączenia czy po nim?
Mam nadzieję, że to pomoże, proszę zadawać więcej pytań w związku z tą odpowiedzią, dołożę wszelkich starań, aby wyjaśnić moje stanowisko.
źródło
Kiedy piszesz, Magician.pullRabbitOutOfInt, dokumentuje to, o czym myślałeś podczas pisania metody. Dzwoniący będzie oczekiwać, że to zadziała, jeśli przekaże dowolną liczbę całkowitą. Kiedy piszesz, Magician.pullRabbitOutOfAnything, dzwoniący nie wie, co myśleć i musi zagłębić się w kod i eksperymentować. Może działać dla Int, ale czy będzie działał przez długi czas? Pływak? Podwójny? Jeśli piszesz ten kod, jak daleko chcesz się posunąć? Jakie argumenty chcesz poprzeć?
Niejednoznaczność wymaga czasu na zrozumienie. Nie jestem nawet przekonany, że szybciej jest pisać:
Vs:
OK, więc dodałem wyjątek do twojego kodu (który bardzo polecam), aby powiedzieć dzwoniącemu, że nigdy nie wyobrażałeś sobie, że przekażą ci to, co zrobili. Ale pisanie konkretnych metod (nie wiem, czy JavaScript to umożliwia) nie jest już kodem i jest znacznie łatwiejsze do zrozumienia jako wywołującego. Ustanawia realistyczne założenia na temat tego, o czym myślał autor tego kodu, i sprawia, że kod jest łatwy w użyciu.
źródło