Dalam desain API, kapan harus menggunakan / menghindari polimorfisme ad hoc?

14

Sue sedang merancang perpustakaan JavaScript Magician.js,. Kunci pasnya adalah fungsi yang menarik Rabbitkeluar dari argumen yang disahkan.

Dia tahu penggunanya mungkin ingin menarik kelinci keluar dari String, a Number, a Function, bahkan mungkin a HTMLElement. Dengan pemikiran itu, ia dapat merancang API-nya seperti:

Antarmuka yang ketat

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

Setiap fungsi dalam contoh di atas akan tahu bagaimana menangani argumen dari jenis yang ditentukan dalam nama fungsi / nama parameter.

Atau, dia bisa mendesainnya seperti ini:

Antarmuka "ad hoc"

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

pullRabbitharus menjelaskan variasi dari berbagai tipe yang diharapkan dari anythingargumen tersebut, serta (tentu saja) tipe yang tidak terduga:

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

Yang pertama (ketat) tampaknya lebih eksplisit, mungkin lebih aman, dan mungkin lebih berkinerja - karena ada sedikit atau tidak ada biaya tambahan untuk pengecekan tipe atau konversi tipe. Tetapi yang terakhir (ad hoc) merasa lebih mudah melihatnya dari luar, karena "hanya bekerja" dengan argumen apa pun yang menurut konsumen nyaman untuk diteruskan.

Untuk jawaban atas pertanyaan ini , saya ingin melihat pro dan kontra khusus untuk pendekatan mana pun (atau pendekatan yang berbeda secara keseluruhan, jika tidak ada yang ideal), bahwa Sue harus tahu pendekatan mana yang harus diambil ketika merancang API perpustakaannya.

Gladstone. Terus
sumber
2
Mengetik bebek
Vorac

Jawaban:

7

Beberapa pro dan kontra

Pro untuk polimorfik:

  • Antarmuka polimorfik yang lebih kecil lebih mudah dibaca. Saya hanya perlu mengingat satu metode.
  • Ini sesuai dengan cara bahasa dimaksudkan untuk digunakan - Mengetik bebek.
  • Jika jelas objek mana yang ingin saya tarik keluar kelinci, seharusnya tidak ada ambiguitas.
  • Melakukan banyak pengecekan tipe dianggap buruk bahkan dalam bahasa statis seperti Java, di mana memiliki banyak pengecekan tipe untuk jenis objek membuat kode jelek, jika penyihir benar-benar perlu membedakan antara jenis objek yang ia tarik keluar kelinci dari ?

Pro untuk ad-hoc:

  • Itu kurang eksplisit, bisakah saya menarik string dari Catinstance? Apakah itu hanya berfungsi? jika tidak, apa perilakunya? Jika saya tidak membatasi jenis di sini, saya harus melakukannya dalam dokumentasi, atau dalam tes yang mungkin membuat kontrak lebih buruk.
  • Anda memiliki semua penanganan menarik kelinci di satu tempat, si Penyihir (beberapa mungkin menganggap ini penipu)
  • Pengoptimal JS modern membedakan antara fungsi monomorfik (hanya bekerja pada satu tipe) dan polimorfik. Mereka tahu cara mengoptimalkan yang monomorfik jauh lebih baik sehingga pullRabbitOutOfStringversi ini kemungkinan akan jauh lebih cepat di mesin seperti V8. Lihat video ini untuk informasi lebih lanjut. Sunting: Saya menulis perf, ternyata dalam prakteknya, ini tidak selalu terjadi .

Beberapa solusi alternatif:

Menurut pendapat saya, desain semacam ini tidak terlalu 'Java-Scripty' untuk memulai. JavaScript adalah bahasa yang berbeda dengan idiom yang berbeda dari bahasa seperti C #, Java atau Python. Idiom-idiom ini berasal dari pengembang bertahun-tahun yang mencoba memahami bagian bahasa yang lemah dan kuat, yang saya lakukan adalah mencoba untuk tetap menggunakan idiom-idiom ini.

Ada dua solusi bagus yang dapat saya pikirkan:

  • Mengangkat objek, membuat objek "menarik", membuatnya sesuai dengan antarmuka saat run-time, kemudian meminta Magician bekerja pada objek yang dapat ditarik.
  • Menggunakan pola strategi, mengajar Pesulap secara dinamis bagaimana menangani berbagai jenis objek.

Solusi 1: Mengangkat Objek

Salah satu solusi umum untuk masalah ini, adalah 'meninggikan' benda dengan kemampuan kelinci ditarik keluar.

Artinya, memiliki fungsi yang mengambil beberapa jenis objek, dan menambahkan menarik topi untuk itu. Sesuatu seperti:

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

Saya dapat membuat makePullablefungsi seperti itu untuk objek lain, saya bisa membuat makePullableString, dll. Saya mendefinisikan konversi pada setiap jenis. Namun, setelah saya mengangkat objek saya, saya tidak memiliki tipe untuk menggunakannya secara umum. Antarmuka dalam JavaScript ditentukan oleh mengetik bebek, jika memiliki pullOfHatmetode saya dapat menariknya dengan metode Magician.

Kemudian Magician bisa melakukan:

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

Mengangkat objek, menggunakan semacam pola mixin sepertinya lebih banyak hal JS yang harus dilakukan. (Catatan ini bermasalah dengan tipe nilai dalam bahasa yang berupa string, angka, null, tidak terdefinisi dan boolean, tetapi semuanya bisa dilakukan dalam kotak)

Berikut adalah contoh dari apa yang terlihat seperti kode

Solusi 2: Pola Strategi

Ketika mendiskusikan pertanyaan ini di ruang obrolan JS di StackOverflow, teman saya phenomnomnominal menyarankan penggunaan pola Strategi .

Ini akan memungkinkan Anda untuk menambahkan kemampuan untuk menarik kelinci keluar dari berbagai objek pada saat dijalankan, dan akan membuat kode yang sangat JavaScript. Seorang pesulap dapat belajar cara menarik benda-benda dari berbagai jenis dari topi, dan itu menarik mereka berdasarkan pengetahuan itu.

Berikut ini tampilannya di CoffeeScript:

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

Anda dapat melihat kode JS yang setara di sini .

Dengan cara ini, Anda mendapat manfaat dari kedua dunia, tindakan cara menarik tidak terlalu erat dengan objek, atau Penyihir dan saya pikir ini membuat solusi yang sangat bagus.

Penggunaan akan menjadi seperti:

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
sumber
1
Saya juga sangat merekomendasikan buku pola desain JavaScript ini
Benjamin Gruenbaum
2
Saya akan memberi 10 ini untuk jawaban yang sangat teliti dari mana saya belajar banyak, tetapi, sesuai aturan SE, Anda harus puas dengan +1 ... :-)
Marjan Venema
@ MarsjanVenema Jawaban lain juga bagus, pastikan untuk membacanya juga. Saya senang Anda menikmati ini. Jangan ragu untuk mampir dan mengajukan pertanyaan desain lainnya.
Benjamin Gruenbaum
4

Masalahnya adalah Anda mencoba menerapkan jenis polimorfisme yang tidak ada dalam JavaScript - JavaScript hampir secara universal paling baik diperlakukan sebagai bahasa yang diketik bebek, meskipun itu mendukung beberapa jenis fakultas.

Untuk membuat API terbaik, jawabannya adalah Anda harus mengimplementasikan keduanya. Ini sedikit lebih banyak mengetik, tetapi akan menghemat banyak pekerjaan dalam jangka panjang untuk pengguna API Anda.

pullRabbitseharusnya hanya menjadi metode arbiter yang memeriksa jenis dan memanggil fungsi yang tepat terkait dengan jenis objek tersebut (misalnya pullRabbitOutOfHtmlElement).

Dengan cara itu, sementara prototipe pengguna dapat menggunakan pullRabbit, tetapi jika mereka melihat perlambatan mereka dapat mengimplementasikan pengecekan tipe di ujung mereka (kemungkinan cara yang lebih cepat) dan pullRabbitOutOfHtmlElementlangsung menelepon .

Jonathan Rich
sumber
2

Ini adalah JavaScript. Ketika Anda melakukannya dengan lebih baik, Anda akan menemukan sering ada jalan tengah yang membantu meniadakan dilema seperti ini. Juga, itu benar-benar tidak masalah apakah 'tipe' yang tidak didukung ditangkap oleh sesuatu atau rusak ketika seseorang mencoba menggunakannya karena tidak ada kompilasi vs run-time. Jika Anda menggunakannya salah itu rusak. Mencoba menyembunyikan bahwa itu rusak atau membuatnya bekerja setengah jalan ketika rusak tidak mengubah fakta bahwa ada sesuatu yang rusak.

Jadi miliki kue Anda dan makanlah juga dan belajarlah untuk menghindari jenis kebingungan dan kerusakan yang tidak perlu dengan menjaga semuanya benar-benar jelas, seperti dalam nama baik dan dengan semua detail yang tepat di semua tempat yang tepat.

Pertama-tama, saya sangat mendorong kebiasaan membasmi bebek Anda secara berurutan sebelum Anda perlu memeriksa jenisnya. Hal yang paling ramping dan paling efisien (tetapi tidak selalu terbaik di mana konstruktor asli yang bersangkutan) yang harus dilakukan adalah untuk memukul prototipe terlebih dahulu sehingga metode Anda bahkan tidak perlu peduli tentang apa yang didukung jenis yang dimainkan.

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

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

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

Catatan: Ini secara luas dianggap sebagai bentuk buruk untuk melakukan ini pada Object karena semuanya mewarisi dari Object. Saya pribadi akan menghindari Fungsi juga. Beberapa orang mungkin merasa ragu menyentuh prototipe konstruktor asli mana pun yang mungkin bukan kebijakan yang buruk tetapi contohnya masih bisa berfungsi ketika bekerja dengan konstruktor objek Anda sendiri.

Saya tidak akan khawatir tentang pendekatan ini untuk metode penggunaan khusus seperti itu yang tidak mungkin akan menghancurkan sesuatu dari perpustakaan lain dalam aplikasi yang kurang rumit, tetapi itu adalah naluri yang baik untuk menghindari menyatakan sesuatu yang terlalu umum di seluruh metode asli dalam JavaScript jika Anda tidak harus kecuali Anda menormalkan metode yang lebih baru di browser yang kedaluwarsa.

Untungnya, Anda selalu dapat hanya memetakan jenis atau nama konstruktor ke metode (berhati-hatilah dengan IE <= 8 yang tidak memiliki <object> .constructor.name yang mengharuskan Anda menguraikannya dari hasil toString dari properti konstruktor). Anda masih berlaku memeriksa nama konstruktor (typeof adalah jenis tidak berguna di JS ketika membandingkan objek) tetapi setidaknya itu membaca jauh lebih bagus daripada pernyataan switch raksasa atau jika / rantai lain dalam setiap panggilan metode untuk apa yang bisa menjadi lebar berbagai benda.

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

Atau menggunakan pendekatan peta yang sama, jika Anda ingin akses ke komponen 'ini' dari berbagai jenis objek untuk menggunakannya seolah-olah mereka adalah metode tanpa menyentuh prototipe yang diwarisi:

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

Prinsip umum yang baik dalam IMO bahasa apa pun, adalah mencoba memilah detail percabangan seperti ini sebelum Anda mendapatkan kode yang benar-benar menarik pelatuknya. Dengan begitu mudah untuk melihat semua pemain yang terlibat di tingkat API atas untuk ikhtisar yang bagus, tetapi juga jauh lebih mudah untuk memilah-milah di mana rincian seseorang mungkin peduli akan ditemukan.

Catatan: ini semua belum teruji, karena saya berasumsi tidak ada yang benar-benar menggunakan RL untuk itu. Saya yakin ada kesalahan ketik / bug.

Erik Reppen
sumber
1

Ini (bagi saya) adalah pertanyaan yang menarik dan rumit untuk dijawab. Saya sebenarnya suka pertanyaan ini jadi saya akan melakukan yang terbaik untuk menjawab. Jika Anda melakukan penelitian apa pun ke dalam standar untuk pemrograman javascript, Anda akan menemukan banyak cara "benar" untuk melakukannya karena ada orang yang menggembar-gemborkan cara "benar" melakukannya.

Tetapi karena Anda sedang mencari pendapat tentang jalan mana yang lebih baik. Tidak ada gunanya.

Saya pribadi lebih suka pendekatan desain "adhoc". Berasal dari latar belakang c ++ / C #, ini lebih merupakan gaya pengembangan saya. Anda dapat membuat satu permintaan pullRabbit dan meminta satu jenis permintaan untuk memeriksa argumen yang masuk dan melakukan sesuatu. Ini berarti Anda tidak perlu khawatir tentang jenis argumen yang diajukan pada satu waktu. Jika Anda menggunakan pendekatan yang ketat, Anda masih perlu memeriksa jenis variabelnya, tetapi Anda akan melakukannya sebelum melakukan pemanggilan metode. Jadi pada akhirnya pertanyaannya adalah, apakah Anda ingin memeriksa jenisnya sebelum melakukan panggilan atau sesudahnya.

Saya harap ini membantu, jangan ragu untuk mengajukan lebih banyak pertanyaan sehubungan dengan jawaban ini, saya akan melakukan yang terbaik untuk memperjelas posisi saya.

Kenneth Garza
sumber
0

Ketika Anda menulis, Magician.pullRabbitOutOfInt, itu mendokumentasikan apa yang Anda pikirkan ketika Anda menulis metode. Penelepon akan mengharapkan ini berfungsi jika melewati bilangan bulat apa pun. Ketika Anda menulis, Magician.pullRabbitOutOfAnything, penelepon tidak tahu harus berpikir apa dan harus menggali kode Anda dan bereksperimen. Ini mungkin bekerja untuk Int, tetapi apakah itu akan bekerja untuk yang lama? Float? A Double? Jika Anda menulis kode ini, seberapa jauh Anda bersedia melangkah? Argumen macam apa yang ingin Anda dukung?

  • String?
  • Array?
  • Peta?
  • Stream?
  • Fungsi?
  • Basis data?

Ambiguitas membutuhkan waktu untuk dipahami. Saya bahkan tidak yakin bahwa lebih cepat menulis:

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, jadi saya menambahkan pengecualian pada kode Anda (yang saya sangat rekomendasikan) untuk memberi tahu penelepon bahwa Anda tidak pernah membayangkan mereka akan memberi Anda apa yang mereka lakukan. Tetapi menulis metode khusus (saya tidak tahu apakah JavaScript memungkinkan Anda melakukan ini) tidak ada lagi kode dan lebih mudah dipahami sebagai penelepon. Ini menetapkan asumsi realistis tentang apa yang dipikirkan oleh pembuat kode ini, dan membuat kode tersebut mudah digunakan.

GlenPeterson
sumber
Hanya memberi tahu Anda bahwa JavaScript memungkinkan Anda melakukan hal itu :)
Benjamin Gruenbaum
Jika hiper-eksplisit lebih mudah dibaca / dimengerti, buku-buku bagaimana akan dibaca seperti legalese. Juga, metode per-tipe yang secara kasar melakukan hal yang sama adalah pelanggaran KERING utama pada JS dev Anda. Nama untuk maksud, bukan untuk tipe. Argumen apa yang diperlukan harus jelas atau sangat mudah dicari dengan memeriksa di satu tempat dalam kode atau dalam daftar arg yang diterima di satu nama metode dalam dokumen.
Erik Reppen