API tasarımında, ad hoc polimorfizm ne zaman kullanılır / kaçınır?


14

Bir JavaScript kütüphanesi tasarlıyor Magician.js. Linchpin, Rabbitgeçirilen argümanın dışına çıkan bir işlevdir .

Kullanıcılarının bir tavşanı a String, a Number, a Function, belki de a'dan çıkarmak isteyebileceğini biliyor HTMLElement. Bunu akılda tutarak, API'sını şöyle tasarlayabilir:

Sıkı arayüz

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

Yukarıdaki örnekteki her işlev, işlev adı / parametre adında belirtilen tür argümanının nasıl işleneceğini bilir.

Ya da şöyle tasarlayabilir:

"Geçici" arayüz

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

pullRabbitanythingbağımsız değişkenin olabileceği farklı beklenen türlerin yanı sıra (elbette) beklenmedik bir tür de hesaba katılması gerekir :

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

Eski (katı), daha açık, belki daha güvenli ve belki de daha performanslı görünüyor - çünkü tip kontrolü veya tip dönüşümü için çok az veya hiç ek yük yoktur. Ancak, ikincisi (ad hoc) dışarıdan bakmak daha basittir, çünkü API tüketicisinin ona geçmek için uygun bulduğu her türlü argümanla "çalışır".

Bu sorunun cevabı için , Sue'nun kütüphanesinin API'sini tasarlarken hangi yaklaşımı alacağını bilmesi gereken her iki yaklaşımın (veya her ikisi de ideal değilse, tamamen farklı bir yaklaşımın) belirli artılarını ve eksilerini görmek istiyorum.


Yanıtlar:


7

Bazı artıları ve eksileri

Polimorfik için artıları:

  • Daha küçük bir polimorfik arayüzün okunması daha kolaydır. Sadece bir yöntemi hatırlamak zorundayım.
  • Dilin kullanılması gerektiği yolla gider - Ördek yazımı.
  • Bir tavşanı hangi nesnelerden çıkarmak istediğim açıksa, yine de belirsizlik olmamalıdır.
  • Sihirbazın tavşan çıkardığı nesnelerin türünü ayırt etmesi gerektiğinde, nesnenin türü için çok sayıda tür kontrolünün çirkin kod oluşturduğu Java gibi statik dillerde bile çok fazla tür denetimi yapmak kötü kabul edilir. ?

Geçici için artıları:

  • Daha az belirgindir, bir dizeden bir Catörnek alabilir miyim? Sadece işe yarar mı? değilse, davranış nedir? Buradaki türü sınırlamazsam, bunu belgelerde veya daha kötü bir sözleşme yapabilecek testlerde yapmak zorundayım.
  • Bir tavşanı tek bir yerde çekmek için tüm işlemlere sahipsiniz, Sihirbaz (bazıları bunu bir con olarak düşünebilir)
  • Modern JS optimizasyonları monomorfik (sadece bir tipte çalışır) ve polimorfik fonksiyonlar arasında farklılık gösterir. Onlar monomorfik olanları nasıl optimize biliyorum çok böylece daha iyi pullRabbitOutOfStringversiyonu olma olasılığı çok V8 gibi motorlarında daha hızlı. Daha fazla bilgi için bu videoyu izleyin. Düzenleme: Kendime bir mükemmel yazdım , pratikte bu her zaman böyle değil .

Bazı alternatif çözümler:

Bence, bu tür bir tasarım başlamak için çok fazla 'Java-Scripty' değil. JavaScript, C #, Java veya Python gibi dillerden farklı deyimlere sahip farklı bir dildir. Bu deyimler, dilin zayıf ve güçlü kısımlarını anlamaya çalışan geliştiricilerin yıllarından kaynaklanır, yapacağım şey bu deyimlere bağlı kalmaya çalışmaktır.

Düşünebileceğim iki güzel çözüm var:

  • Nesneleri kaldırmak, nesneleri "ezilebilir" yapmak, onları çalışma zamanında bir arayüze uydurmak, ardından Sihirbazın ezilebilir nesneler üzerinde çalışması.
  • Strateji modelini kullanarak Büyücüye farklı türdeki nesnelerin nasıl işleneceğini dinamik olarak öğretin.

Çözüm 1: Nesneleri Kaldırma

Bu soruna ortak bir çözüm, tavşanları çekip çıkarma yeteneğiyle nesneleri 'yükseltmektir'.

Yani, bir tür nesne alan ve onun için şapkadan çekmeyi ekleyen bir işleve sahip olmak. Gibi bir şey:

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

Ben makePullablediğer nesneler için böyle işlevler yapabilirsiniz , ben oluşturabilir makePullableString, vb. Ben her tür üzerinde dönüşüm tanımlamak. Ancak, nesnelerimi yükselttikten sonra, bunları genel bir şekilde kullanacak bir türüm yok. JavaScript bir arayüz, bir varsa, bir ördek yazarak tarafından belirlenir pullOfHatyöntemi ben yapabilirsiniz Magician'ların yöntemiyle çekin.

Sonra Sihirbaz yapabilirdi:

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

Nesneleri yükseltmek, bir çeşit mixin deseni kullanmak daha fazla JS işi gibi görünüyor. (Bunun dilde, sayı, null, undefined ve boolean dilde değer türleriyle ilgili sorunlu olduğunu unutmayın, ancak hepsi kutuda kullanılabilir)

İşte böyle bir kodun nasıl görünebileceğine bir örnek

Çözüm 2: Strateji Kalıbı

StackOverflow'daki JS sohbet odasında bu soruyu tartışırken arkadaşım fenomnomnominal Strateji modelinin kullanılmasını önerdi .

Bu, çalışma zamanında tavşanları çeşitli nesnelerden çekme yeteneklerini eklemenize izin verir ve çok JavaScript kodu oluşturur. Bir sihirbaz, farklı tipteki nesnelerin şapkadan nasıl çekileceğini öğrenebilir ve onları bu bilgiye dayanarak çeker.

Bu, CoffeeScript'te şöyle görünebilir:

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

Eşdeğer JS kodunu burada görebilirsiniz .

Bu şekilde, her iki dünyadan da faydalanırsınız, nasıl çekileceği eylemi nesnelere veya Sihirbaza sıkı sıkıya bağlı değildir ve bence bu çok güzel bir çözüm sağlar.

Kullanım şöyle bir şey olurdu:

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");


2
Ben çok şey öğrendim çok kapsamlı bir cevap için +10 olurdu, ama, SE kurallarına göre, +1 ... :-) için yerleşmek zorunda
kalacak

@MarjanVenema Diğer cevaplar da iyi, onları da mutlaka okuyun. Bundan hoşlandığın için mutluyum. Uğramaktan çekinmeyin ve daha fazla tasarım sorusu sorun.
Benjamin Gruenbaum

4

Sorun şu ki, JavaScript'te bulunmayan bir tür polimorfizm uygulamaya çalışıyorsunuz - JavaScript, bazı tür fakülteleri desteklese bile, neredeyse evrensel olarak ördek tipi bir dil olarak ele alınır.

En iyi API'yı oluşturmak için yanıt, her ikisini de uygulamanızdır. Biraz daha yazıyor, ancak API'nızın kullanıcıları için uzun vadede çok fazla iş tasarrufu sağlayacak.

pullRabbityalnızca türleri denetleyen ve bu nesne türüyle (ör. pullRabbitOutOfHtmlElement) ilişkili doğru işlevi çağıran bir arabulucu yöntemi olmalıdır .

Bu şekilde, prototipleme yaparken kullanıcılar kullanabilir pullRabbit, ancak bir yavaşlama fark ederse, tür denetimlerini sonuna uygulayabilirler (muhtemelen daha hızlı bir şekilde) ve pullRabbitOutOfHtmlElementdoğrudan arayın .


2

Bu JavaScript. Onu daha iyi hale getirdikçe, genellikle böyle ikilemleri reddetmeye yardımcı olan bir orta yol olduğunu göreceksiniz. Ayrıca, desteklenmeyen bir 'türün' bir şey tarafından yakalanıp yakalanmadığı ya da birisinin kullanmaya çalıştığında kırılması önemli değildir, çünkü derleme ve çalışma zamanı yoktur. Yanlış kullanırsanız kırılır. Kırıldığını gizlemeye çalışmak veya kırıldığında yarıya kadar çalışmasını sağlamak, bir şeyin kırıldığı gerçeğini değiştirmez.

Bu yüzden pastanızı da yiyin ve yiyin ve her şeyi gerçekten, gerçekten açık, iyi adlandırılmış ve tüm doğru yerlerde doğru ayrıntılarla koruyarak karışıklık ve gereksiz kırılmayı önlemeyi öğrenin.

Her şeyden önce, türleri kontrol etmeden önce ördeklerinizi arka arkaya alma alışkanlığına girmenizi şiddetle tavsiye ederim. Yapılacak en yalın ve en verimli (ancak yerli inşaatçılar söz konusu olduğunda her zaman en iyisi değil) ilk önce prototiplere çarpmak olacaktır, böylece yönteminizin desteklenen türün ne olduğunu umursamasına gerek kalmaz.

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

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

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

Not: Nesneye her şey Object'ten miras alındığında bunu Object'e yapmak yaygın olarak kötü bir form olarak kabul edilir. Ben şahsen Fonksiyondan da kaçınırdım. Bazıları, herhangi bir yerel kurucunun prototipine dokunmak konusunda kötü bir politika olabilir, ancak bu kötü bir politika olmayabilir, ancak örnek yine de kendi nesne kurucularınızla çalışırken kullanılabilir.

Daha az karmaşık bir uygulamada başka bir kütüphaneden bir şey bulması muhtemel olmayan böyle bir özel kullanım yöntemi için bu yaklaşım hakkında endişelenmeyeceğim, ancak genellikle JavaScript'teki yerel yöntemlerde aşırı bir şey iddia etmekten kaçınmak iyi bir içgüdü eski tarayıcılarda daha yeni yöntemleri normalleştirmediğiniz sürece yapmanız gerekir.

Neyse ki, her zaman türleri veya yapıcı adlarını yöntemlerle önceden eşleştirebilirsiniz (yapıcı özelliğinden toString sonuçlarından ayrıştırmanızı gerektiren <object> .constructor.name dosyasına sahip olmayan IE <= 8'e dikkat edin). Hala yapıcı adını kontrol ediyorsun (typeof, nesneleri karşılaştırırken JS'de bir tür işe yaramaz), ancak en azından dev bir anahtar deyiminden veya yöntemin her çağrısında geniş / geniş olabilir ne / else zincirinde çok daha güzel okur nesnelerin çeşitliliği.

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]();
}

Ya da aynı harita yaklaşımını kullanarak, farklı nesne türlerinin 'bu' bileşenine, miras alınan prototiplerine dokunmadan yöntemmiş gibi kullanmak istiyorsanız:

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(' ');
    );
}

Herhangi bir dil IMO'sunda iyi bir genel prensip, aslında tetiği çeken koda gitmeden önce bu gibi dallanma ayrıntılarını sıralamaya çalışmaktır. Bu şekilde, iyi bir genel bakış için en üst API düzeyinde yer alan tüm oyuncuları görmek kolaydır, ancak birisinin umursadığı ayrıntıların nerede bulunabileceğini bulmak çok daha kolaydır.

Not: bunların hepsi test edilmemiştir, çünkü kimsenin bunun için bir RL kullanımı olmadığını varsayalım. Yazım hataları / hatalar olduğundan eminim.


1

Bu benim için ilginç ve karmaşık bir soru. Aslında bu soruyu seviyorum, bu yüzden cevaplamak için elimden geleni yapacağım. Javascript programlama standartlarında herhangi bir araştırma yaparsanız, bunu yapmanın "doğru" yolunu tanıtan insanlar olduğu için bunu yapmanın birçok "doğru" yolunu bulacaksınız.

Ama hangi yolun daha iyi olduğuna dair bir fikir arıyorsanız. İşte hiçbir şey yok.

Ben şahsen "adhoc" tasarım yaklaşımını tercih ederim. Bir c ++ / C # arka planından geliyor, bu daha çok benim gelişim tarzım. Bir pullRabbit isteği oluşturabilir ve bir istek türünün iletilen bağımsız değişkeni kontrol etmesini ve bir şey yapmasını sağlayabilirsiniz. Bu, herhangi bir anda ne tür bir argümanın aktarıldığından endişelenmenize gerek olmadığı anlamına gelir. Katı yaklaşımı kullanırsanız, değişkenin hangi tür olduğunu kontrol etmeniz gerekir, ancak yöntem çağrısını yapmadan önce bunu yaparsınız. Sonuçta soru şu ki, arama yapmadan önce veya sonra tipini kontrol etmek istiyor musunuz?

Umarım bu yardımcı olur, lütfen bu cevapla ilgili daha fazla soru sormaktan çekinmeyin, pozisyonumu netleştirmek için elimden geleni yapacağım.


0

Yazdığınızda, Magician.pullRabbitOutOfInt, yöntemi yazdığınızda ne düşündüğünüzü belgeler. Arayan, herhangi bir Tamsayı geçtiğinde bunun çalışmasını bekler. Yazarken, Magician.pullRabbitOutOfAnything, arayan ne düşüneceğini bilmiyor ve kodunuzu kazmaya ve denemeye gitmek zorunda. Bir Int için işe yarayabilir, ancak bir Long için işe yarayacak mı? Su üstünde? Bir çift? Bu kodu yazıyorsanız, ne kadar ileri gitmeye hazırsınız? Ne tür argümanları desteklemeye hazırsınız?

  • Teller?
  • Diziler?
  • Haritalar?
  • Canlı Yayınlar?
  • Fonksiyonlar?
  • Veritabanları?

Belirsizlik anlaşılması zaman alır. Yazmanın daha hızlı olduğuna bile ikna olmadım:

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

Tamam, bu yüzden (ki kesinlikle tavsiye ederim) kodunuza bir istisna ekledi arayan asla onlar ne yaptığını size hayal hayal. Ama belirli yöntemler (JavaScript bunu yapmanıza izin verir mi bilmiyorum) yazmak artık kod ve arayan olarak anlamak çok daha kolay. Bu kodun yazarının ne düşündüğü hakkında gerçekçi varsayımlar kurar ve kodun kullanımını kolaylaştırır.


Sadece JavaScript'in bunu yapmanıza izin verdiğini bilmenizi isterim :)
Benjamin Gruenbaum

Eğer hiper-müstehcen okunması / anlaşılması daha kolay olsaydı, nasıl yapılır kitapları yasal gibi okunurdu. Ayrıca, kabaca aynı şeyi yapan tip başına yöntemler, tipik JS geliştiriciniz için büyük bir DRY faulüdür. Niyetin adı, tür için değil. Hangi argümanların açık olması ya da koddaki bir yerde kontrol edilmesi ya da bir dokümanın tek bir yöntem adında kabul edilen argümanlar listesinde bulunması çok kolay olmalıdır.
Erik Reppen
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.