ในการออกแบบ API ควรใช้ / หลีกเลี่ยง Ad Hoc polymorphism เมื่อใด


14

ซูคือการออกแบบห้องสมุด Magician.jsJavaScript, Linchpin มันเป็นฟังก์ชั่นที่ดึงRabbitออกมาจากการโต้แย้งผ่าน

เธอรู้ว่าผู้ใช้อาจต้องการที่จะดึงกระต่ายออกจากStringการNumberเป็นบางทีแม้กระทั่งFunction HTMLElementเมื่อคำนึงถึงสิ่งนี้เธอสามารถออกแบบ API ของเธอได้ดังนี้:

อินเตอร์เฟซที่เข้มงวด

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

แต่ละฟังก์ชั่นในตัวอย่างด้านบนจะรู้วิธีจัดการอาร์กิวเมนต์ประเภทที่ระบุในชื่อฟังก์ชัน / ชื่อพารามิเตอร์

หรือเธอสามารถออกแบบได้เช่น:

อินเทอร์เฟซ "เฉพาะกิจ"

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

pullRabbitจะต้องพิจารณาถึงความหลากหลายของประเภทที่คาดหวังที่แตกต่างกันซึ่งการanythingโต้แย้งอาจเป็นประเภทที่ไม่คาดคิด (แน่นอน):

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

อดีต (เข้มงวด) ดูเหมือนชัดเจนมากขึ้นอาจปลอดภัยกว่าและมีประสิทธิภาพมากกว่า - เนื่องจากมีค่าโสหุ้ยน้อยหรือไม่มีเลยสำหรับการตรวจสอบประเภทหรือการแปลงประเภท แต่อันหลัง (เฉพาะกิจ) รู้สึกง่ายกว่าเมื่อมองจากด้านนอกในที่นี้ "ใช้ได้" กับทุกสิ่งที่ผู้บริโภค API พบว่าสะดวกต่อการส่งผ่าน

สำหรับคำตอบของคำถามนี้ฉันต้องการเห็นข้อดีและข้อเสียของวิธีการอย่างใดอย่างหนึ่ง (หรือแนวทางที่แตกต่างกันโดยสิ้นเชิงหากไม่เหมาะสม) ซูควรรู้วิธีการที่จะใช้เมื่อออกแบบ API ห้องสมุดของเธอ


คำตอบ:


7

ข้อดีและข้อเสียบางอย่าง

ข้อดีสำหรับ polymorphic:

  • ส่วนต่อประสาน polymorphic ที่เล็กกว่านั้นง่ายต่อการอ่าน ฉันต้องจำวิธีเดียวเท่านั้น
  • มันไปพร้อมกับวิธีการใช้ภาษา - การพิมพ์เป็ด
  • ถ้ามันชัดเจนว่าวัตถุใดที่ฉันต้องการดึงกระต่ายออกมาก็ไม่ควรมีความกำกวมอยู่ดี
  • การทำการตรวจสอบประเภทจำนวนมากถือว่าไม่ดีแม้ในภาษาแบบคงที่เช่น Java ซึ่งการตรวจสอบประเภทจำนวนมากสำหรับประเภทของวัตถุสร้างรหัสที่น่าเกลียดนักมายากลควรจะแยกแยะความแตกต่างระหว่างประเภทของวัตถุที่เขาดึงกระต่ายออกมา ?

ข้อดีสำหรับเฉพาะกิจ:

  • มีความชัดเจนน้อยกว่าฉันสามารถดึงสตริงออกมาจากCatอินสแตนซ์ได้หรือไม่ มันจะใช้ได้ไหม ถ้าไม่ใช่พฤติกรรมคืออะไร? หากฉันไม่ จำกัด ประเภทที่นี่ฉันต้องทำในเอกสารหรือในการทดสอบที่อาจทำให้สัญญาแย่ลง
  • คุณมีการจัดการกับการดึงกระต่ายในที่เดียวนักมายากล (บางคนอาจคิดว่านี่เป็นการต่อต้าน)
  • เครื่องมือเพิ่มประสิทธิภาพ JS สมัยใหม่แตกต่างกันระหว่าง monomorphic (ทำงานกับประเภทเดียว) และฟังก์ชัน polymorphic พวกเขารู้วิธีที่จะเพิ่มประสิทธิภาพคน monomorphic มากดีกว่าดังนั้นpullRabbitOutOfStringรุ่นมีแนวโน้มที่จะมากได้เร็วขึ้นในเครื่องมือเช่น V8 ดูวิดีโอนี้สำหรับข้อมูลเพิ่มเติม แก้ไข:ฉันเขียน perf ตัวเองปรากฎว่าในทางปฏิบัตินี่ไม่ใช่กรณีเสมอไป

ทางเลือกอื่น ๆ :

ในความคิดของฉันการออกแบบประเภทนี้ไม่ได้เป็น 'Java-Scripty' มากนัก JavaScript เป็นภาษาที่แตกต่างกันและมีสำนวนต่าง ๆ จากภาษาเช่น C #, Java หรือ Python สำนวนเหล่านี้เกิดขึ้นในหลายปีของนักพัฒนาที่พยายามเข้าใจส่วนที่อ่อนแอและแข็งแกร่งของภาษาสิ่งที่ฉันทำคือพยายามที่จะยึดติดกับสำนวนเหล่านี้

มีวิธีแก้ปัญหาที่ดีสองข้อที่ฉันนึกได้:

  • การยกระดับวัตถุทำให้วัตถุ "ดึงได้" ทำให้วัตถุนั้นสอดคล้องกับอินเทอร์เฟซในเวลาทำงานจากนั้นให้นักมายากลทำงานบนวัตถุที่ดึงได้
  • ใช้รูปแบบกลยุทธ์การสอนผู้วิเศษแบบไดนามิกวิธีการจัดการวัตถุชนิดต่าง ๆ

โซลูชันที่ 1: วัตถุที่ยกระดับ

ทางออกหนึ่งที่พบบ่อยในการแก้ไขปัญหานี้คือการ 'ยกระดับ' วัตถุด้วยความสามารถในการดึงกระต่ายออกมาจากพวกมัน

นั่นคือมีฟังก์ชั่นที่ใช้วัตถุบางชนิดและเพิ่มการดึงหมวกออกมา สิ่งที่ต้องการ:

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

ฉันสามารถสร้างmakePullableฟังก์ชั่นดังกล่าวสำหรับวัตถุอื่นฉันสามารถสร้างmakePullableStringฯลฯ ฉันกำหนดการแปลงในแต่ละประเภท อย่างไรก็ตามหลังจากที่ฉันยกระดับวัตถุของฉันฉันไม่มีประเภทที่จะใช้พวกเขาในทางทั่วไป อินเทอร์เฟซใน JavaScript ถูกกำหนดโดยการพิมพ์เป็ดหากมีpullOfHatวิธีฉันสามารถดึงมันด้วยวิธีการของนักมายากล

จากนั้นนักมายากลก็สามารถทำได้:

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

การยกระดับวัตถุโดยใช้รูปแบบมิกซ์อินบางประเภทดูเหมือนว่าจะต้องทำสิ่งเพิ่มเติมใน JS (โปรดทราบว่านี่เป็นปัญหากับประเภทค่าในภาษาซึ่งเป็นสตริง, จำนวน, null, ไม่ได้กำหนดและบูลีน แต่พวกมันทั้งหมดสามารถใช้กล่องได้)

นี่คือตัวอย่างของรหัสดังกล่าวที่อาจมีลักษณะเหมือน

โซลูชันที่ 2: รูปแบบกลยุทธ์

เมื่อพูดถึงคำถามนี้ในห้องแชท JS ใน StackOverflow เพื่อนของฉันphenomnomnominalแนะนำให้ใช้ของรูปแบบกลยุทธ์

สิ่งนี้จะช่วยให้คุณสามารถเพิ่มความสามารถในการดึงกระต่ายออกจากวัตถุต่าง ๆ ในขณะใช้งานและจะสร้างโค้ดจาวาสคริปต์ นักมายากลสามารถเรียนรู้วิธีการดึงวัตถุประเภทต่าง ๆ ออกมาจากหมวกและดึงพวกเขาตามความรู้นั้น

นี่คือลักษณะที่ปรากฏใน 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

ท่านสามารถเข้าดูเทียบเท่ารหัส JS ที่นี่

ด้วยวิธีนี้คุณจะได้รับประโยชน์จากทั้งสองโลกการกระทำของวิธีการดึงไม่ได้เชื่อมโยงอย่างแน่นหนากับวัตถุหรือนักมายากลและฉันคิดว่านี่เป็นวิธีการแก้ปัญหาที่ดีมาก

การใช้งานจะเป็นสิ่งที่ชอบ:

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
ฉันต้องการ 10 ข้อนี้สำหรับคำตอบที่ละเอียดมากซึ่งฉันได้เรียนรู้มากมาย แต่ตามกฎของ SE คุณจะต้องชำระค่า +1 ... :-)
Marjan Venema

@MarjanVenema คำตอบอื่น ๆ ก็ดีเช่นกันอย่าลืมอ่านมันด้วย ฉันดีใจที่คุณสนุกกับสิ่งนี้ รู้สึกอิสระที่จะหยุดและถามคำถามการออกแบบเพิ่มเติม
Benjamin Gruenbaum

4

ปัญหาคือคุณกำลังพยายามใช้ความหลากหลายที่ไม่มีอยู่ใน JavaScript - JavaScript ได้รับการปฏิบัติที่ดีที่สุดในระดับสากลว่าเป็นภาษาที่พิมพ์ออกมาเป็ดแม้ว่ามันจะรองรับคณะบางประเภทก็ตาม

เพื่อสร้าง API ที่ดีที่สุดคำตอบคือคุณควรใช้ทั้งสองอย่าง มันค่อนข้างจะพิมพ์ได้มากขึ้น แต่จะช่วยประหยัดงานได้มากมายในระยะยาวสำหรับผู้ใช้ API ของคุณ

pullRabbitควรเป็นวิธีการตัดสินที่ตรวจสอบประเภทและเรียกใช้ฟังก์ชันที่เหมาะสมที่เกี่ยวข้องกับประเภทวัตถุนั้น (เช่นpullRabbitOutOfHtmlElement)

ด้วยวิธีนี้ในขณะที่ผู้ใช้ต้นแบบสามารถใช้pullRabbitแต่ถ้าพวกเขาสังเกตเห็นการชะลอตัวพวกเขาสามารถใช้การตรวจสอบประเภทที่ปลายของพวกเขา (น่าจะเป็นวิธีที่เร็วขึ้น) และเพียงโทรpullRabbitOutOfHtmlElementโดยตรง


2

นี่คือ JavaScript เมื่อคุณทำได้ดีขึ้นคุณจะพบว่ามีถนนสายกลางที่มักจะช่วยขจัดอุปสรรคเช่นนี้ นอกจากนี้มันไม่สำคัญว่า 'ประเภท' ที่ไม่รองรับจะถูกจับโดยบางสิ่งบางอย่างหรือหยุดพักเมื่อมีคนพยายามใช้งานเพราะไม่มีการคอมไพล์เทียบกับเวลาทำงาน ถ้าคุณใช้มันผิดมันจะหยุดพัก การพยายามปิดบังว่ามันพังหรือทำให้มันทำงานได้ครึ่งทางเมื่อมันพังไม่ได้เปลี่ยนความจริงที่ว่ามีบางอย่างเสีย

ให้เค้กของคุณและกินมันด้วยและเรียนรู้ที่จะหลีกเลี่ยงความสับสนประเภทและความแตกแยกที่ไม่จำเป็นโดยการทำให้ทุกอย่างชัดเจนจริงๆเช่นเดียวกับชื่อที่ดีและรายละเอียดที่ถูกต้องในสถานที่ที่เหมาะสม

ก่อนอื่นเลยฉันขอแนะนำให้คุณลองทำเป็ดให้เป็นนิสัยก่อนที่จะตรวจสอบประเภท สิ่งที่น้อยที่สุดและมีประสิทธิภาพมากที่สุด (แต่ไม่เสมอไปที่ดีที่สุดที่นักสร้างดั้งเดิมมีความกังวล) สิ่งที่ต้องทำคือการตีต้นแบบก่อนเพื่อให้วิธีการของคุณไม่ต้องสนใจว่าจะรองรับประเภทใดในการเล่น

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

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

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

หมายเหตุ: มันถือว่าเป็นรูปแบบที่ไม่ดีในการทำเช่นนี้กับ Object เนื่องจากทุกสิ่งสืบทอดจาก Object ฉันเองก็จะหลีกเลี่ยง Function ด้วยเช่นกัน บางคนอาจรู้สึกโมโหเกี่ยวกับการสัมผัสต้นแบบของนวกรรมิกพื้นเมืองซึ่งอาจไม่ใช่นโยบายที่ไม่ดี แต่ตัวอย่างก็ยังสามารถแสดงเมื่อทำงานกับตัวสร้างวัตถุของคุณเอง

ฉันไม่ต้องกังวลเกี่ยวกับวิธีการนี้สำหรับวิธีการใช้งานเฉพาะที่ไม่น่าจะปิดบังบางสิ่งบางอย่างจากห้องสมุดอื่นในแอพที่ซับซ้อนน้อยลง แต่มันเป็นสัญชาตญาณที่ดีในการหลีกเลี่ยงการทำอะไรมากเกินไปกับวิธีการทั่วไปใน JavaScript ต้องเว้นแต่คุณจะทำวิธีการใหม่ให้เป็นมาตรฐานในเบราว์เซอร์ที่ล้าสมัย

โชคดีที่คุณสามารถ pre-map type หรือชื่อ constructor ไปยังเมธอดได้เสมอ (ระวัง IE <= 8 ที่ไม่มี <object> .constructor.name ที่คุณต้องแยกมันออกจากผลลัพธ์ toString จากคุณสมบัติ Constructor) คุณยังคงตรวจสอบชื่อคอนสตรัคเตอร์ (typeof นั้นไร้ประโยชน์ใน JS เมื่อเปรียบเทียบวัตถุ) แต่อย่างน้อยมันก็อ่านได้ดีกว่าคำแถลงสวิทช์ยักษ์หรือถ้า / เชนอื่น ๆ ในการเรียกใช้เมธอดถึงความกว้าง ความหลากหลายของวัตถุ

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

หรือใช้วิธีการแมปเดียวกันถ้าคุณต้องการเข้าถึงส่วนประกอบ 'this' ของประเภทวัตถุที่แตกต่างกันเพื่อใช้พวกเขาราวกับว่าพวกเขาเป็นวิธีการโดยไม่ต้องสัมผัสต้นแบบต้นแบบที่สืบทอดมา:

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

หลักการทั่วไปที่ดีใน IMO ภาษาใด ๆ คือการพยายามที่จะแยกแยะรายละเอียดการแยกสาขาแบบนี้ก่อนที่คุณจะได้รับรหัสที่ดึงทริกเกอร์ ด้วยวิธีนี้มันง่ายที่จะเห็นผู้เล่นทุกคนที่เกี่ยวข้องกับระดับ API ด้านบนนั้นสำหรับภาพรวมที่ดี แต่ยังง่ายกว่ามากในการแยกแยะว่ารายละเอียดที่ใครบางคนสนใจอาจพบได้ที่ไหน

หมายเหตุ: ทั้งหมดนี้ยังไม่ได้ทดสอบเพราะฉันคิดว่าไม่มีใครสามารถใช้ RL ได้จริง ฉันแน่ใจว่ามีข้อผิดพลาด / ข้อผิดพลาด


1

(สำหรับฉัน) นี้เป็นคำถามที่น่าสนใจและซับซ้อนในการตอบ ฉันชอบคำถามนี้ดังนั้นฉันจะพยายามอย่างดีที่สุดที่จะตอบ หากคุณทำการวิจัยใด ๆ ลงในมาตรฐานสำหรับการเขียนโปรแกรมจาวาสคริปต์คุณจะพบวิธีที่ "ถูกต้อง" ในการทำเช่นนั้นเพราะมีคนโน้มน้าววิธี "ถูกต้อง"

แต่เนื่องจากคุณกำลังมองหาความคิดเห็นว่าทางไหนดีกว่ากัน ที่นี่ไม่มีอะไรไป

โดยส่วนตัวฉันชอบวิธีการออกแบบ "adhoc" มาจากพื้นหลัง c ++ / C # นี่คือรูปแบบการพัฒนาของฉัน คุณสามารถสร้างคำขอ pullRabbit หนึ่งคำขอและให้คำขอประเภทนั้นตรวจสอบอาร์กิวเมนต์ที่ส่งผ่านและทำบางสิ่ง ซึ่งหมายความว่าคุณไม่ต้องกังวลว่าจะมีการโต้แย้งประเภทใดในครั้งเดียว หากคุณใช้วิธีการที่เข้มงวดคุณจะต้องตรวจสอบว่าตัวแปรชนิดใด แต่คุณควรทำก่อนที่จะทำการเรียกเมธอด ดังนั้นในที่สุดคำถามคือคุณต้องการตรวจสอบประเภทก่อนที่จะโทรหรือหลัง

ฉันหวังว่าสิ่งนี้จะช่วยได้โปรดอย่าลังเลที่จะถามคำถามเพิ่มเติมเกี่ยวกับคำตอบนี้ฉันจะพยายามอย่างเต็มที่เพื่ออธิบายตำแหน่งของฉัน


0

เมื่อคุณเขียน Magician.pullRabbitOutOfInt จะบันทึกสิ่งที่คุณคิดเมื่อคุณเขียนวิธี ผู้เรียกจะคาดหวังว่าสิ่งนี้จะทำงานได้ถ้าผ่าน Integer ใด ๆ เมื่อคุณเขียน Magician.pullRabbitOutOfAny สิ่งที่ผู้โทรไม่รู้ว่าจะคิดอย่างไรและต้องขุดลงไปในโค้ดของคุณและทำการทดลอง มันอาจใช้ได้กับ Int แต่จะใช้ได้นานไหม โฟลต คู่ หากคุณกำลังเขียนรหัสนี้คุณยินดีที่จะไปไกลแค่ไหน? คุณต้องการสนับสนุนข้อโต้แย้งประเภทใด

  • สตริง?
  • อาร์เรย์?
  • แผนที่
  • ลำธาร?
  • ฟังก์ชั่น?
  • ฐานข้อมูล?

ความกำกวมต้องใช้เวลาในการทำความเข้าใจ ฉันไม่เชื่อด้วยซ้ำว่าจะเขียนได้เร็วกว่า:

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

ตกลงดังนั้นฉันจึงเพิ่มข้อยกเว้นในรหัสของคุณ (ซึ่งฉันขอแนะนำเป็นอย่างยิ่ง) เพื่อบอกผู้โทรว่าคุณไม่เคยคิดเลยว่าพวกเขาจะผ่านสิ่งที่คุณทำไป แต่การเขียนวิธีการเฉพาะ (ฉันไม่ทราบว่า JavaScript ช่วยให้คุณทำสิ่งนี้) ไม่มีรหัสเพิ่มเติมและเข้าใจได้ง่ายกว่าในฐานะผู้โทร มันตั้งสมมติฐานที่เป็นจริงเกี่ยวกับสิ่งที่ผู้เขียนรหัสนี้คิดและทำให้ใช้งานได้ง่าย


เพียงแค่ให้คุณทราบว่า JavaScript ช่วยให้คุณทำอย่างนั้น :)
เบนจามิน Gruenbaum

หากการอ่านและเข้าใจง่ายเกินไปนั้นง่ายกว่าหนังสือวิธีใช้จะอ่านเหมือนถูกกฎหมาย นอกจากนี้วิธีการต่อประเภทที่ทุกคนทำในสิ่งเดียวกันนั้นเป็นสิ่งที่มีความสำคัญมากต่อนักพัฒนา JS ทั่วไปของคุณ ชื่อสำหรับเจตนาไม่ใช่สำหรับประเภท สิ่งที่ต้องใช้จะต้องชัดเจนหรือง่ายต่อการค้นหาโดยการตรวจสอบที่เดียวในรหัสหรือในรายการของ args ที่ยอมรับได้ที่ชื่อวิธีการหนึ่งในเอกสาร
Erik Reppen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.