在API设计中,何时使用/避免临时多态性?


14

Sue正在设计一个JavaScript库Magician.js。它的关键是Rabbit从传递的参数中拉出一个函数。

她知道它的用户可能想从a String,a Number,a Function甚至甚至是a中拔出一只兔子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使用者认为方便传递给它的任何参数。

对于这个问题的答案,我想看看哪种方法(或完全不同的方法,如果都不是理想的话)的利弊,那么Sue在设计她的库的API时应该知道采用哪种方法。


Answers:


7

一些利弊

多态的优点:

  • 较小的多态界面更易于阅读。我只需要记住一种方法。
  • 它与该语言的使用方式配合使用-鸭子打字。
  • 如果很清楚我想把兔子拉出来的对象,也就不会有歧义。
  • 即使在像Java这样的静态语言中,进行大量类型检查也被认为是不好的,如果魔术师确实需要区分他从中拔出兔子的对象类型,那么对对象类型进行大量类型检查会使代码变得丑陋。 ?

临时的优点:

  • 它不太明确,可以从Cat实例中拉出字符串吗?这样行得通吗?如果没有,那是什么行为?如果我不在这里限制类型,则必须在文档或可能会使合同更糟的测试中进行限制。
  • 您可以将魔术师拉到一个兔子的地方(有些人可能会认为这是骗局)
  • 现代JS优化器区分单态(仅对一种类型有效)和多态函数。他们知道如何优化单态的人更好,所以该pullRabbitOutOfString版本很可能是快像V8引擎。观看此视频以获取更多信息。 编辑:我自己写了一个性能,事实证明,在实践中,并非总是如此

一些替代解决方案:

在我看来,这种设计一开始并不是很“ Java脚本”。JavaScript是一种与C#,Java或Python等语言不同的成语。这些习惯用法源于多年开发人员试图理解该语言的弱点和强项的过程,我要做的是尝试坚持使用这些习惯用法。

我可以想到两种不错的解决方案:

  • 提升对象,使对象“可拉”,使其在运行时符合接口,然后由魔术师处理可拉对象。
  • 使用策略模式,动态地教魔术师如何处理不同类型的对象。

解决方案1:提升对象

解决此问题的一种常见方法是“提升”物体,使其具有将兔子拉出的能力。

就是说,有一个接受某种类型物体的功能,并为此增加了拉出帽子的功能。就像是:

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

我可以makePullable为其他对象创建此类函数,也可以创建一个makePullableString,等等。我在每种类型上定义转换。但是,在提升对象之后,我便没有类型以通用方式使用它们。JavaScript中的接口由鸭子类型决定,如果它具有pullOfHat方法I可以用魔术师的方法将其拉出。

然后魔术师可以做:

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

提升对象,使用某种混合模式似乎更像是JS。(请注意,这对于使用字符串,数字,null,未定义和布尔值的语言中的值类型是有问题的,但是它们都是可装箱的)

这是此类代码的示例

解决方案2:策略模式

在StackOverflow的JS聊天室中讨论这个问题时,我的朋友phennomnomnominal建议使用Strategy模式

这将允许您添加在运行时将兔子从各种对象中拉出的功能,并创建非常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");


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进行此操作被广泛认为是一种不好的形式。我个人也避免使用Function。有些人可能会对接触任何本机构造函数的原型感到不安,这可能不是一个坏策略,但是在使用自己的对象构造函数时,该示例仍然可以使用。

我不会为这种特定用途的方法担心这种方法,这种方法不太可能破坏不太复杂的应用程序中另一个库中的内容,但是如果您不这样做的话,最好避免在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编程标准进行任何研究,就会发现有许多“吹捧”正确方法的人。

但是,由于您正在寻找哪种方法更好的意见。这里什么也没有。

我个人更喜欢“临时”设计方法。来自c ++ / C#背景,这更是我的开发风格。您可以创建一个pullRabbit请求,并让一种请求类型检查传入的参数并执行某些操作。这意味着您不必担心随时传递哪种类型的参数。如果使用严格的方法,则仍然需要检查变量的类型,但是在进行方法调用之前,您需要这样做。因此,最后的问题是,您要在拨打电话之前还是之后检查类型。

希望对您有所帮助,请随时提出有关此答案的更多问题,我会尽力澄清自己的立场。


0

在编写Magician.pullRabbitOutOfInt时,它记录了编写该方法时的想法。如果传递了任何Integer,则调用方将期望此方法有效。在编写Magician.pullRabbitOutOfAnything时,调用者不知道要怎么想,因此必须深入研究代码并进行实验。它可能适用于Int,但会适用于Long吗?漂浮?一双?如果您正在编写此代码,您愿意走多远?您愿意支持哪种论点?

  • 琴弦?
  • 数组?
  • 地图?
  • 流?
  • 功能?
  • 数据库?

模糊性需要时间来理解。我什至不相信写起来会更快:

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使您可以做到这一点:)
Benjamin Gruenbaum

如果超明文更易于阅读/理解,则操作方法书的阅读方式将像法律术语。而且,几乎都执行相同操作的按类型方法对典型的JS开发人员来说是主要的DRY犯规。名称用于意图,而非类型。通过检查代码中的某个位置或在文档中某个方法名称的可接受的args列表中查找,所需的args应该显而易见或非常容易。
Erik Reppen 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.