В разработке API, когда использовать / избегать специального полиморфизма?

14

Сью разрабатывает библиотеку JavaScript Magician.js. Его стержень является функцией, которая извлекает Rabbitиз переданного аргумента.

Она знает, что ее пользователи могут захотеть вытащить кролика из a String, a Number, a Function, возможно, даже a 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 своей библиотеки.

GladstoneKeep
источник
2
Печатание

Ответы:

7

Некоторые плюсы и минусы

Плюсы для полиморфизма:

  • Меньший полиморфный интерфейс легче читать. Я должен помнить только один метод.
  • Это соответствует тому, как должен использоваться язык - Duck typing.
  • Если ясно, из каких объектов я хочу вытащить кролика, двусмысленности не должно быть.
  • Выполнение большого количества проверок типов считается плохим даже в статических языках, таких как Java, где наличие большого количества проверок типов для типа объекта создает уродливый код, если магу действительно нужно различать типы объектов, из которых он вытаскивает кролика. ?

Плюсы для ad-hoc:

  • Это менее явно, я могу вытащить строку из 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-код. Маг может научиться извлекать предметы разных типов из шляп, и он вытягивает их на основе этих знаний.

Вот как это может выглядеть в 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");
Бенджамин Грюнбаум
источник
1
Я также очень рекомендую эту бесплатную книгу «Шаблоны проектирования JavaScript»
Бенджамин Грюнбаум
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 при сравнении объектов), но, по крайней мере, он читается намного лучше, чем гигантский оператор switch или цепочка 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]();
}

Или с использованием того же подхода к карте, если вы хотите, чтобы доступ к компоненту «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

Это (для меня) интересный и сложный вопрос. Мне действительно нравится этот вопрос, поэтому я сделаю все возможное, чтобы ответить. Если вы вообще исследуете стандарты программирования на javascript, вы найдете столько же «правильных» способов сделать это, сколько и людей, которые рекламируют «правильный» способ сделать это.

Но так как вы ищете мнение о том, какой путь лучше. Тут ничего не происходит.

Я лично предпочел бы подход дизайна "adhoc". Исходя из c ++ / C # фона, это скорее мой стиль разработки. Вы можете создать один запрос pullRabbit и сделать так, чтобы один тип запроса проверял передаваемый аргумент и что-то делал. Это означает, что вам не нужно беспокоиться о том, какой тип аргумента передается в любое время. Если вы используете строгий подход, вам все равно нужно будет проверить, к какому типу относится переменная, но вместо этого вы должны сделать это, прежде чем выполнять вызов метода. Итак, в конце концов, вопрос заключается в том, хотите ли вы проверить тип до того, как вы сделаете звонок или после.

Я надеюсь, что это поможет, пожалуйста, не стесняйтесь задавать больше вопросов относительно этого ответа, я сделаю все возможное, чтобы уточнить мою позицию.

Кеннет Гарза
источник
0

Когда вы пишете Magician.pullRabbitOutOfInt, он документирует то, о чем вы думали, когда писали метод. Вызывающая сторона будет ожидать, что это сработает, если передается любое целое число. Когда вы пишете Magician.pullRabbitOutOfAnything, вызывающая сторона не знает, что думать, и должна копаться в вашем коде и экспериментировать. Это может работать для Int, но будет ли работать долго? Поплавок? Двойной? Если вы пишете этот код, как далеко вы готовы пойти? Какие аргументы вы готовы поддержать?

  • Строки?
  • Массивы?
  • Карты?
  • Streams?
  • Функции?
  • Базы данных?

Двусмысленность требует времени, чтобы понять. Я даже не уверен, что быстрее написать

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 это сделать) больше не является кодом, и его гораздо легче понять как вызывающего. Он устанавливает реалистичные предположения о том, что задумал автор этого кода, и делает код простым в использовании.

GlenPeterson
источник
Просто сообщаю вам, что JavaScript позволяет вам делать это :)
Бенджамин Грюнбаум
Если бы гипер-явное было легче читать / понимать, книги с практическими рекомендациями читались бы как легальные. Кроме того, методы для каждого типа, которые примерно одинаково делают одно и то же, являются основным СУХИМЫМ фолом для вашего типичного разработчика JS. Имя для намерения, а не для типа. То, какие аргументы требуются, должно быть либо очевидным, либо очень легко найти, проверив одно место в коде или список принятых аргументов одного имени метода в документе.
Эрик Реппен