Wann sollte im API-Design Ad-hoc-Polymorphismus verwendet / vermieden werden?

14

Sue entwirft eine JavaScript - Bibliothek Magician.js. Sein Dreh- und Angelpunkt ist eine Funktion, die ein Rabbitaus dem übergebenen Argument zieht .

Sie weiß, dass seine Benutzer ein Kaninchen aus einem String, einem Number, einem Function, vielleicht sogar einem herausziehen möchten HTMLElement. In diesem Sinne könnte sie ihre API folgendermaßen gestalten:

Die strenge Schnittstelle

Magician.pullRabbitOutOfString = function(str) //...
Magician.pullRabbitOutOfHTMLElement = function(htmlEl) //...

Jede Funktion im obigen Beispiel kann mit dem Argument des im Funktionsnamen / Parameternamen angegebenen Typs umgehen.

Oder sie könnte es so gestalten:

Die "Ad-hoc" -Schnittstelle

Magician.pullRabbit = function(anything) //...

pullRabbitmüsste die Vielfalt der erwarteten Typen berücksichtigen, die das anythingArgument haben könnte, sowie (natürlich) einen unerwarteten 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.
};

Ersteres (strengeres) scheint eindeutiger, vielleicht sicherer und vielleicht leistungsfähiger zu sein - da für die Typprüfung oder die Typkonvertierung nur geringe oder keine zusätzlichen Kosten anfallen. Letzteres (ad hoc) fühlt sich jedoch von außen einfacher an, da es mit jedem Argument "funktioniert", das der API-Konsument für angebracht hält, um es weiterzugeben.

Bei der Beantwortung dieser Frage würde ich mir wünschen, dass Sue bestimmte Vor- und Nachteile für einen der beiden Ansätze (oder für einen anderen Ansatz insgesamt, wenn keiner der beiden ideal ist) kennt und weiß, welchen Ansatz sie beim Entwerfen der API ihrer Bibliothek verfolgen soll.

GladstoneKeep
quelle
2
Duck Typing
Vorac

Antworten:

7

Einige Vor- und Nachteile

Vorteile für polymorphe:

  • Eine kleinere polymorphe Schnittstelle ist leichter zu lesen. Ich muss mich nur an eine Methode erinnern.
  • Es hängt von der Art und Weise ab, wie die Sprache verwendet werden soll - Duck Typing.
  • Wenn klar ist, aus welchen Objekten ich ein Kaninchen herausziehen möchte, sollte es sowieso keine Mehrdeutigkeit geben.
  • Eine große Menge an Typüberprüfungen wird auch in statischen Sprachen wie Java als schlecht angesehen, wo eine große Menge an Typüberprüfungen für den Typ des Objekts hässlichen Code erzeugt, falls der Zauberer wirklich zwischen den Objekttypen unterscheiden muss, aus denen er ein Kaninchen herauszieht ?

Vorteile für Ad-hoc:

  • Es ist weniger explizit. Kann ich eine Zeichenfolge aus einer CatInstanz ziehen? Würde das einfach funktionieren? Wenn nicht, wie ist das Verhalten? Wenn ich den Typ hier nicht einschränke, muss ich das in der Dokumentation oder in den Tests tun, die einen schlechteren Vertrag machen könnten.
  • Sie haben die ganze Mühe, ein Kaninchen an einem Ort zu ziehen, dem Magier (einige mögen dies als Betrug ansehen).
  • Moderne JS-Optimierer unterscheiden zwischen monomorphen (funktioniert nur für einen Typ) und polymorphen Funktionen. Sie wissen, wie man die monomorphen viel besser optimiert, sodass die pullRabbitOutOfStringVersion in Motoren wie V8 wahrscheinlich viel schneller ist. Weitere Informationen finden Sie in diesem Video. Edit: Ich habe selbst einen Perf geschrieben, es stellt sich heraus, dass dies in der Praxis nicht immer der Fall ist .

Einige alternative Lösungen:

Meiner Meinung nach ist diese Art von Design anfangs nicht sehr "Java-Scripty". JavaScript ist eine andere Sprache mit anderen Redewendungen als C #, Java oder Python. Diese Redewendungen stammen aus Jahren von Entwicklern, die versuchen, die schwachen und starken Teile der Sprache zu verstehen. Ich würde versuchen, mich an diese Redewendungen zu halten.

Ich kann mir zwei schöne Lösungen vorstellen:

  • Objekte anheben, Objekte "abziehbar" machen, sie zur Laufzeit an eine Schnittstelle anpassen und dann den Magier an abziehbaren Objekten arbeiten lassen.
  • Unter Verwendung des Strategiemusters kann der Magier dynamisch lernen, mit verschiedenen Arten von Objekten umzugehen.

Lösung 1: Objekte anheben

Eine gebräuchliche Lösung für dieses Problem besteht darin, Gegenstände mit der Fähigkeit, Kaninchen herausziehen zu lassen, anzuheben.

Das heißt, Sie haben eine Funktion, die eine Art Objekt annimmt und das Herausziehen eines Hutes für dieses hinzufügt. Etwas wie:

function makePullable(obj){
   obj.pullOfHat = function(){
       return new Rabbit(obj.toString());
   }
}

Ich kann solche makePullableFunktionen für andere Objekte erstellen, ich kann eine erstellen makePullableStringusw. Ich definiere die Konvertierung für jeden Typ. Nachdem ich meine Objekte angehoben habe, kann ich sie jedoch nicht generisch verwenden. Eine Schnittstelle in JavaScript wird durch eine Enten-Eingabe bestimmt. Wenn es eine pullOfHatMethode gibt, kann ich sie mit der Magician-Methode ziehen.

Dann könnte Magier tun:

Magician.pullRabbit = function(pullable) {
    var rabbit = obj.pullOfHat();
    return {rabbit:rabbit,text:"Tada, I pulled a rabbit out of "+pullable};
}

Das Erhöhen von Objekten mithilfe eines Mix-In-Musters scheint das Richtigere für JS zu sein. (Beachten Sie, dass dies bei Werttypen in der Sprache problematisch ist, bei denen es sich um Zeichenfolgen, Zahlen, Nullen, Undefinierte und Boolesche Werte handelt, die jedoch alle boxfähig sind.)

Hier ist ein Beispiel, wie ein solcher Code aussehen könnte

Lösung 2: Strategiemuster

Als ich diese Frage im JS-Chatroom in StackOverflow diskutierte, schlug mein Freund phenomnomnominal die Verwendung des Strategiemusters vor .

Dies würde es Ihnen ermöglichen, die Fähigkeiten zum Ziehen von Kaninchen aus verschiedenen Objekten zur Laufzeit hinzuzufügen und einen sehr JavaScript-fähigen Code zu erstellen. Ein Zauberer kann lernen, wie er Gegenstände verschiedener Arten aus Hüten zieht, und er zieht sie basierend auf diesem Wissen.

So könnte das in CoffeeScript aussehen:

class Magician
  constructor: ()-> # A new Magician can't pull anything
     @pullFunctions = {}

  pullRabbit: (obj) -> # Pull a rabbit, handler based on type
    func = pullFunctions[obj.constructor.name]
    if func? then func(obj) else "Don't know how to pull that out of my hat!"

  learnToPull: (obj, handler) -> # Learns to pull a rabbit out of a type
    pullFunctions[obj.constructor.name] = handler

Sie können den entsprechenden JS-Code hier sehen .

Auf diese Weise profitieren Sie von beiden Welten. Die Aktion des Ziehens ist weder eng mit den Objekten noch mit dem Magier verbunden, und ich denke, dies ist eine sehr schöne Lösung.

Die Verwendung würde ungefähr so ​​aussehen:

var m = new Magician();//create a new Magician
//Teach the Magician
m.learnToPull("",function(){
   return "Pulled a rabbit out of a string";
});
m.learnToPull({},function(){
   return "Pulled a rabbit out of a Object";
});

m.pullRabbit(" Str");
Benjamin Gruenbaum
quelle
2
Ich würde dies für eine sehr gründliche Antwort, aus der ich viel gelernt habe, auf +10 setzen, aber gemäß den SE-Regeln müssen Sie sich mit +1 begnügen ... :-)
Marjan Venema,
@ MarjanVenema Die anderen Antworten sind auch gut, lesen Sie sie auch. Ich bin froh, dass dir das gefallen hat. Schauen Sie doch einfach mal vorbei und stellen Sie weitere Designfragen.
Benjamin Gruenbaum
4

Das Problem ist, dass Sie versuchen, eine Art von Polymorphismus zu implementieren, die in JavaScript nicht vorhanden ist. JavaScript wird im Allgemeinen am besten als ententypisierte Sprache behandelt, obwohl es einige Typenfakultäten unterstützt.

Um die beste API zu erstellen, müssen Sie beide implementieren. Es ist ein bisschen mehr Tipparbeit, spart aber auf lange Sicht viel Arbeit für Benutzer Ihrer API.

pullRabbitsollte nur eine Arbiter-Methode sein, die Typen prüft und die richtige Funktion aufruft, die diesem Objekttyp zugeordnet ist (z pullRabbitOutOfHtmlElement. B. ).

Auf diese Weise können Benutzer zwar Prototyping verwenden pullRabbit, aber wenn sie eine Verlangsamung bemerken, können sie die Typprüfung an ihrem Ende implementieren (wahrscheinlich schneller) und einfach pullRabbitOutOfHtmlElementdirekt anrufen .

Jonathan Rich
quelle
2

Dies ist JavaScript. Wenn Sie es besser machen, werden Sie feststellen, dass es oft einen Mittelweg gibt, der hilft, solche Dilemmata zu überwinden. Außerdem spielt es keine Rolle, ob ein nicht unterstützter 'Typ' von etwas abgefangen wird oder abbricht, wenn jemand versucht, ihn zu verwenden, weil es keine Kompilierung oder Laufzeit gibt. Wenn Sie es falsch verwenden, bricht es. Der Versuch zu verbergen, dass es kaputt gegangen ist oder auf halbem Weg funktioniert, wenn es kaputt gegangen ist, ändert nichts an der Tatsache, dass etwas kaputt gegangen ist.

Nehmen Sie also Ihren Kuchen mit und essen Sie ihn auch und lernen Sie, Typverwechslungen und unnötige Brüche zu vermeiden, indem Sie alles wirklich, wirklich offensichtlich und mit den richtigen Details an den richtigen Stellen aufbewahren.

Zuallererst ermutige ich Sie sehr, sich daran zu gewöhnen, Ihre Enten hintereinander zu bringen, bevor Sie die Typen überprüfen müssen. Am schlanksten und effizientesten (aber nicht immer am besten, wenn es um native Konstruktoren geht) ist es, zuerst die Prototypen zu testen, damit sich Ihre Methode nicht einmal darum kümmern muss, welcher unterstützte Typ im Spiel ist.

String.prototype.pullRabbit = function(){
    //do something string-relevant
}

HTMLElement.prototype.pullRabbit = function(){
    //do something HTMLElement-relevant
}

Magician.pullRabbitFrom = function(someThingy){
    return someThingy.pullRabbit();
}

Hinweis: Es wird allgemein als schlechte Form angesehen, dies mit Object zu tun, da alles von Object erbt. Ich persönlich würde die Funktion auch meiden. Einige fühlen sich vielleicht ärgerlich, wenn sie den Prototyp eines nativen Konstruktors anfassen, was vielleicht keine schlechte Richtlinie ist, aber das Beispiel kann trotzdem bei der Arbeit mit Ihren eigenen Objektkonstruktoren hilfreich sein.

Ich würde mir keine Sorgen um diesen Ansatz für eine Methode machen, die für eine bestimmte Verwendung geeignet ist und in einer weniger komplizierten App wahrscheinlich nichts aus einer anderen Bibliothek herausholt, aber es ist ein guter Instinkt, zu vermeiden, dass bei systemeigenen Methoden in JavaScript etwas zu allgemein behauptet wird, wenn dies nicht der Fall ist müssen, es sei denn, Sie normalisieren neuere Methoden in veralteten Browsern.

Glücklicherweise können Sie Typen oder Konstruktornamen immer nur vorab auf Methoden abbilden (achten Sie darauf, dass IE <= 8 nicht <object> .constructor.name enthält, sodass Sie es aus den toString-Ergebnissen der Konstruktoreigenschaft analysieren müssen). Sie überprüfen immer noch den Konstruktornamen (typeof ist in JS beim Vergleichen von Objekten irgendwie nutzlos), aber es liest sich zumindest viel besser als eine riesige switch-Anweisung oder if / else-Kette in jedem Aufruf der Methode, die eine breite sein könnte Vielzahl von Objekten.

var rabbitPullMap = {
    String: ( function pullRabbitFromString(){
        //do stuff here
    } ),
    //parens so we can assign named functions if we want for helpful debug
    //yes, I've been inconsistent. It's just a nice unrelated trick
    //when you want a named inline function assignment

    HTMLElement: ( function pullRabitFromHTMLElement(){
        //do stuff here
    } )
}

Magician.pullRabbitFrom = function(someThingy){
    return rabbitPullMap[someThingy.constructor.name]();
}

Oder verwenden Sie denselben Kartenansatz, wenn Sie möchten, dass Sie auf die 'this'-Komponente der verschiedenen Objekttypen zugreifen können, um sie so zu verwenden, als wären sie Methoden, ohne ihre vererbten Prototypen zu berühren:

var rabbitPullMap = {
    String: ( function(obj){

    //yes the anon wrapping funcs would make more sense in one spot elsewhere.

        return ( function pullRabbitFromString(obj){
            var rabbitReach = this.match(/rabbit/g);
            return rabbitReach.length;
        } ).call(obj);
    } ),

    HTMLElement: ( function(obj){
        return ( function pullRabitFromHTMLElement(obj){
            return this.querySelectorAll('.rabbit').length;
        } ).call(obj);
    } )
}

Magician.pullRabbitFrom = function(someThingy){

    var
        constructorName = someThingy.constructor.name,
        rabbitCnt = rabbitPullMap[constructorName](someThingy);

    console.log(
        [
            'The magician pulls ' + rabbitCnt,
            rabbitCnt === 1 ? 'rabbit' : 'rabbits',
            'out of her ' + constructorName + '.',
            rabbitCnt === 0 ? 'Boo!' : 'Yay!'
        ].join(' ');
    );
}

Ein gutes allgemeines Prinzip in jeder Sprache IMO ist, zu versuchen, Verzweigungsdetails wie diese zu sortieren, bevor Sie zu Code gelangen, der tatsächlich den Auslöser drückt. Auf diese Weise ist es einfach, alle beteiligten Spieler auf dieser obersten API-Ebene zu sehen, um einen guten Überblick zu erhalten, aber es ist auch viel einfacher herauszufinden, wo die Details zu finden sind, die jemanden interessieren könnten.

Hinweis: Dies ist alles ungetestet, da ich annehme, dass niemand eine RL-Verwendung dafür hat. Ich bin mir sicher, dass es Tippfehler gibt.

Erik Reppen
quelle
1

Dies ist (für mich) eine interessante und komplizierte Frage, die zu beantworten ist. Eigentlich mag ich diese Frage, also werde ich mein Bestes geben, um sie zu beantworten. Wenn Sie überhaupt nach Standards für die Javascript-Programmierung suchen, werden Sie so viele "richtige" Methoden finden, wie es Leute gibt, die die "richtige" Methode anpreisen.

Aber da suchst du eine Meinung, welcher Weg besser ist. Hier geht nichts.

Ich persönlich würde den "adhoc" -Designansatz vorziehen. Aus einem C ++ / C # -Hintergrund kommend, ist dies eher mein Entwicklungsstil. Sie können eine PullRabbit-Anfrage erstellen und diesen einen Anfragetyp das übergebene Argument überprüfen lassen, um etwas zu tun. Sie müssen sich also keine Gedanken darüber machen, welche Art von Argument zu einem bestimmten Zeitpunkt übergeben wird. Wenn Sie den strikten Ansatz verwenden, müssten Sie immer noch prüfen, welcher Typ die Variable ist. Stattdessen müssen Sie dies tun, bevor Sie den Methodenaufruf ausführen. Am Ende stellt sich die Frage, ob Sie den Typ vor oder nach dem Anruf überprüfen möchten.

Ich hoffe, dies hilft, bitte zögern Sie nicht, weitere Fragen in Bezug auf diese Antwort zu stellen, ich werde mein Bestes tun, um meine Position zu klären.

Kenneth Garza
quelle
0

Wenn Sie "Magician.pullRabbitOutOfInt" schreiben, wird dokumentiert, worüber Sie beim Schreiben der Methode nachgedacht haben. Der Aufrufer erwartet, dass dies funktioniert, wenn eine Ganzzahl übergeben wird. Wenn Sie schreiben, Magician.pullRabbitOutOfAnything, weiß der Anrufer nicht, was er denken soll, und muss sich in Ihren Code vertiefen und experimentieren. Es könnte für einen Int funktionieren, aber wird es für einen Long funktionieren? Ein Schwimmer? Ein Double? Wenn Sie diesen Code schreiben, wie weit sind Sie bereit zu gehen? Welche Argumente möchten Sie unterstützen?

  • Streicher?
  • Arrays?
  • Karten?
  • Streams?
  • Funktionen?
  • Datenbanken?

Mehrdeutigkeit braucht Zeit zum Verstehen. Ich bin nicht einmal davon überzeugt, dass es schneller ist zu schreiben:

Magician.pullRabbit = function(anything) {
  if (anything === undefined) {
    return new Rabbit(); // out of thin air
  } else if (isString(anything)) {
    // more
  } else if (isNumber(anything)) {
    // more
  } else {
      throw new Exception("You can't pull a rabbit out of that!");
  }
  // etc.
};

Vs:

Magician.pullRabbitFromAir = fromAir() {
    return new Rabbit(); // out of thin air
}
Magician.pullRabbitFromStr = fromString(str)) {
    // more
}
Magician.pullRabbitFromInt = fromInt(int)) {
    // more
};

OK, also habe ich Ihrem Code eine Ausnahme hinzugefügt (die ich sehr empfehle), um dem Anrufer mitzuteilen, dass Sie sich nie vorgestellt haben, dass er Sie überholen würde, was er getan hat. Das Schreiben bestimmter Methoden (ich weiß nicht, ob JavaScript dies zulässt) ist jedoch kein Code mehr und als Aufrufer viel einfacher zu verstehen. Es werden realistische Annahmen darüber getroffen, woran der Autor dieses Codes gedacht hat, und der Code ist einfach zu verwenden.

GlenPeterson
quelle
Nur damit Sie wissen, dass Sie das mit JavaScript können :)
Benjamin Gruenbaum
Wenn hyper-explizit leichter zu lesen / verstehen wäre, würden Anleitungen wie legal gelesen. Per-Type-Methoden, die alle ungefähr dasselbe tun, sind für einen typischen JS-Entwickler ein schwerwiegendes DRY-Foul. Name für Absicht, nicht für Typ. Welche Argumente es braucht, sollte entweder offensichtlich oder sehr einfach nachzuschlagen sein, indem an einer Stelle im Code oder in einer Liste der akzeptierten Argumente unter einem Methodennamen in einem Dokument nachgesehen wird.
Erik Reppen