Коли в дизайні API потрібно використовувати / уникати спеціального поліморфізму?


14

Сью проектуванні бібліотеки JavaScript, Magician.js. Його linchpin - це функція, яка витягує Rabbitаргумент, що передається.

Вона знає, що його користувачі можуть захотіти витягнути кролика з String, а Number, а Function, можливо, навіть із HTMLElement. Зважаючи на це, вона могла б так розробити свій API:

Суворий інтерфейс

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

Кожна функція у наведеному вище прикладі буде знати, як обробити аргумент типу, зазначеного в імені функції / імені параметра.

Або вона могла б спроектувати це так:

Інтерфейс "ad hoc"

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

Колишній (суворий) видається більш явним, можливо, безпечнішим і, можливо, більш ефективним - оскільки накладні витрати для перевірки типу або перетворення типів невеликі або відсутні. Але останній (ad hoc) відчуває себе простіше, дивлячись на нього ззовні, оскільки він "просто працює" з будь-яким аргументом, який споживач API вважає зручним передати йому.

Для відповіді на це питання я хотів би побачити конкретні плюси і мінуси будь-якого підходу (або взагалі іншого підходу, якщо жоден з них не є ідеальним), щоб Сью повинна знати, який підхід використовувати при розробці API своєї бібліотеки.


Відповіді:


7

Деякі плюси і мінуси

Плюси поліморфних:

  • Менший поліморфний інтерфейс легше читати. Мені залишається пам'ятати лише один метод.
  • Це йде з тим, як передбачається використовувати мову - набирання качок.
  • Якщо зрозуміло, з яких предметів я хочу витягнути кролика, все одно не повинно бути двозначності.
  • Проводити багато перевірки типу вважається поганим навіть у статичних мовах, таких як Java, де наявність великої кількості перевірок типу об'єкта робить некрасивий код, якщо фокусник дійсно повинен розмежовувати тип об’єктів, з яких він витягує кролика з ?

Плюси для спеціальних заходів:

  • Це менш явно, чи можу я витягнути рядок із Catекземпляра? Це би просто спрацювало? якщо ні, то яка поведінка? Якщо я не обмежую тут тип, я повинен це зробити в документації або в тестах, які можуть погіршити контракт.
  • У вас є все поводження з витягуванням кролика в одному місці, фокусник (дехто може вважати це недоліком)
  • Сучасні оптимізатори JS розрізняють мономорфну ​​(працює лише на один тип) і поліморфну ​​функції. Вони знають, як оптимізувати мономорфні моменти набагато краще, тому pullRabbitOutOfStringверсія, швидше за все, буде набагато швидшою в таких двигунах, як V8. Дивіться це відео для отримання додаткової інформації. Редагувати: Я написав парфум сам, виявляється, що на практиці це не завжди так .

Деякі альтернативні рішення:

На мою думку, такий дизайн не дуже «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. (Зверніть увагу, це проблематично для типів значень у мові, які є рядковими, числовими, нульовими, невизначеними та булевими, але всі вони мають поле)

Ось приклад того, як може виглядати такий код

Рішення 2: Шаблон стратегії

Під час обговорення цього питання в кімнаті чату JS в StackOverflow мій друг феноменально запропонував використовувати шаблон стратегії .

Це дозволить вам додати можливості витягувати кроликів з різних об'єктів під час виконання, і створив би дуже JavaScript'y код. Маг може навчитися витягувати з капелюхів предмети різного типу, і він витягує їх на основі цих знань.

Ось як це може виглядати в 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 Інші відповіді також хороші, обов’язково прочитайте їх і ви. Я радий, що вам сподобалось це. Не соромтеся заїжджати і задавати більше питань дизайну.
Бенджамін Груенбаум

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. Я особисто уникав би Функції теж. Дехто може відчути гніт щодо дотику до прототипу будь-якого нативного конструктора, що може бути не поганою політикою, але приклад може все-таки слугувати при роботі з власними конструкторами об'єктів.

Я б не хвилювався з приводу такого підходу для такого методу конкретного використання, який, швидше за все, не може прикути щось із іншої бібліотеки в менш складному додатку, але це хороший інстинкт, щоб уникнути твердження нічого надмірно загальноприйнятих методів у JavaScript, якщо ви цього не зробите повинні, якщо ви не нормалізуєте новіші методи у застарілих браузерах.

На щастя, ви завжди можете просто заздалегідь накреслити типи або назви конструкторів на методи (остерігайтеся IE <= 8, у якого немає <object> .constructor.name, що вимагає аналізувати його з результатів toString з властивості конструктора). Ви все ще на ділі перевіряєте ім'я конструктора (typeof є нічим не потрібним в JS при порівнянні об'єктів), але принаймні він читає набагато приємніше, ніж гігантський вимикач переключення або if / else ланцюжок у кожному виклику методу на те, що може бути широким різноманітність об’єктів.

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

Або використовуючи той самий підхід до карти, якщо ви хочете отримати доступ до компонента "цей" різних типів об'єктів, щоб використовувати їх так, ніби вони були методами, не торкаючись їх успадкованих прототипів:

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

Це (для мене) цікаве і складне питання, на яке потрібно відповісти. Мені справді подобається це питання, тому я зроблю все можливе, щоб відповісти. Якщо ви взагалі проводите будь-які дослідження стандартів програмування javascript, ви знайдете стільки «правильних» способів зробити це, скільки людей, які підказують «правильний» спосіб цього зробити.

Але оскільки ви шукаєте думку про те, який спосіб краще. Тут нічого не йде.

Я особисто віддав перевагу "adhoc" дизайнерському підходу. Виходячи з фону c ++ / C #, це більше мій стиль розвитку. Ви можете створити один запит pullRabbit і мати такий тип запиту, щоб перевірити переданий аргумент і щось зробити. Це означає, що вам не доведеться турбуватися про те, який тип аргументу передається будь-коли. Якщо ви використовуєте суворий підхід, вам все одно доведеться перевірити тип типу змінної, але замість цього ви зробите це перед тим, як здійснити виклик методу. Отже, врешті-решт питання полягає в тому, чи бажаєте ви перевірити тип перед тим, як дзвонити або після.

Я сподіваюся, що це допомагає. Будь ласка, не соромтесь задавати більше питань стосовно цієї відповіді, я зроблю все можливе, щоб уточнити свою позицію.


0

Коли ви пишете, Magician.pullRabbitOutOfInt, він документує те, про що ви думали, коли писали метод. Абонент очікує, що це спрацює, якщо передано будь-який цілий номер. Коли ви пишете, Magician.pullRabbitOutOfAnything, той, хто телефонує, не знає, що думати, і повинен заглиблюватися у свій код та експериментувати. Це може працювати для 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 дозволяє вам це робити :)
Бенджамін Груенбаум

Якби гіпер-явні було легше читати / розуміти, книги з технічними документами читали б як легальні. Крім того, методи на тип, які всі роблять те саме, є серйозною помилкою на сухий характер для вашого типового розробника JS. Назва для наміру, а не для типу. Що потрібні аргументи повинні бути очевидними або дуже легко шукати, перевіряючи в одному місці в коді або в списку прийнятих аргументів на одне ім'я методу в документі.
Ерік Реппен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.