web-dev-qa-db-ja.com

API設計で、アドホックポリモーフィズムをいつ使用/回避しますか?

スーはJavaScriptライブラリMagician.jsを設計しています。そのリンピンは、渡された引数からRabbitを引き出す関数です。

彼女は、ユーザーがStringNumberFunction、場合によっては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を設計しています。

14
GladstoneKeep

いくつかの長所と短所

多態性の長所:

  • ポリモーフィックインターフェイスが小さいほど読みやすくなります。覚えなければならないのは1つの方法だけです。
  • それは、言語が使用されることを意図されている方法、つまりダックタイピングと一致します。
  • どのオブジェクトをウサギから引き出すかが明確であれば、あいまいさはありません。
  • マジシャンがウサギを引っ張っているオブジェクトのタイプを区別する必要がある場合、Javaのような静的言語でも多くのタイプチェックを行うことは悪いと考えられています。 ?

アドホックのメリット:

  • それほど明確ではありませんが、Catインスタンスから文字列をプルできますか?それはうまくいくでしょうか?そうでない場合、動作は何ですか?ここでタイプを制限しない場合は、ドキュメントまたはテストで制限する必要があるため、契約が悪化する可能性があります。
  • あなたは1か所のマジシャンでウサギを引っ張るすべての処理を持っています(これを詐欺と考える人もいます)
  • 最新のJSオプティマイザーは、単形関数(1つのタイプでのみ機能)と多形関数を区別します。彼らはモノモーフィックなものを最適化する方法を知っておりはるかにpullRabbitOutOfStringバージョンははるかに速くなる可能性が高いです V8のようなエンジン。 詳細については、このビデオを参照してください。編集:私は自分でパフォーマンスを記述しましたが、実際には 実際には、これは常にそうであるとは限りません

いくつかの代替ソリューション:

私の意見では、この種のデザインはそもそも「Java-Scripty」ではありません。 JavaScriptは、C#、JavaまたはPythonなどの言語とは異なるイディオムを持つ別の言語です。これらのイディオムは、言語の弱点と長所を理解しようとする長年の開発者に由来します。私がやろうとしていることは、これらのイディオムに固執する。

私が考えることができる2つの素晴らしい解決策があります:

  • オブジェクトを昇格させ、オブジェクトを「プル可能」にし、実行時にそれらをインターフェイスに準拠させてから、Magicianにプル可能なオブジェクトを処理させます。
  • 戦略パターンを使用して、さまざまなタイプのオブジェクトの処理方法をマジシャンに動的に教えます。

解決策1:オブジェクトを昇格させる

この問題の一般的な解決策の1つは、ウサギを引き抜く機能を使ってオブジェクトを「持ち上げる」ことです。

つまり、ある種のオブジェクトを受け取り、帽子を引く機能を追加する関数を用意します。何かのようなもの:

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

他のオブジェクトに対してそのようなmakePullable関数を作成したり、makePullableStringなどを作成したりできます。各タイプの変換を定義しています。しかし、オブジェクトを昇格させた後、それらを一般的な方法で使用するための型がありません。 JavaScriptのインターフェースは、pullOfHatメソッドがある場合、ダックタイピングによって決定されます。IできるMagicianのメソッドでそれをプルできます。

その後、マジシャンは次のことができます:

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チャットルームでこの質問について話し合うとき、私の友人 phenomnomnominalStrategy pattern の使用を提案しました。

これにより、実行時にさまざまなオブジェクトからウサギを引き抜く機能を追加でき、非常に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コード here を確認できます。

このようにして、両方の世界から恩恵を受けることができます。プルする方法のアクションは、オブジェクトと魔術師のどちらにも密接に結びついていません。これは非常に良い解決策になると思います。

使用法は次のようになります。

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");
7

問題は、JavaScriptに存在しないポリモーフィズムのタイプを実装しようとしていることです。JavaScriptは、いくつかのタイプの機能をサポートしていますが、ほとんどの場合、アヒル型の言語として最もよく扱われます。

最高のAPIを作成するための答えは、両方を実装する必要があるということです。タイピングは少し増えますが、APIのユーザーにとって、長期的には多くの作業を節約できます。

pullRabbitは、タイプをチェックし、そのオブジェクトタイプに関連付けられた適切な関数を呼び出すアービターメソッド(たとえば、pullRabbitOutOfHtmlElement)である必要があります。

そうすれば、プロトタイピング中にユーザーはpullRabbitを使用できますが、速度が低下していることに気付いた場合は、端に型チェックを実装し(おそらくより高速に)、pullRabbitOutOfHtmlElementを直接呼び出すだけです。

4
Jonathan Rich

これは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のネイティブメソッド全体で過度に一般的に何もアサートしないようにするのは良い本能です。古いブラウザで新しいメソッドを正規化している場合を除き、.

幸いにも、常に型またはコンストラクター名をメソッドに事前にマップすることができます(<object> .constructor.nameを持たないIE <= 8には、コンストラクタープロパティからの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]();
}

Or同じマップアプローチを使用して、異なるオブジェクトタイプの「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を使用している人はいないと思いますので、これはすべてテストされていません。タイプミス/バグがあると確信しています。

2
Erik Reppen

これは(私にとって)興味深い、複雑な質問です。私は実際にこの質問が好きなので、最善を尽くして回答します。 JavaScriptプログラミングの標準について少しでも調査すると、「正しい」方法を宣伝する人々と同じくらい多くの「正しい」方法を見つけることができます。

しかし、あなたはどちらの方法がより良いかについての意見を探しているので。ここでは何もしません。

私は個人的には「アドホック」なデザインアプローチを好みます。 C++/C#のバックグラウンドから来た、これはより私の開発スタイルです。 1つのpullRabbitリクエストを作成し、その1つのリクエストタイプで渡された引数を確認して何かを行うことができます。つまり、一度に渡される引数のタイプについて心配する必要はありません。厳密なアプローチを使用する場合でも、変数のタイプを確認する必要がありますが、代わりにメソッドを呼び出す前に確認します。結局のところ、問題は、電話をかける前または後でタイプを確認することです。

この情報がお役に立てば幸いです。この回答に関して他にもご質問がございましたら、お気軽にお問い合わせください。

1
Kenneth Garza

Magician.pullRabbitOutOfIntを記述すると、メソッドを記述したときに何を考えたかが文書化されます。呼び出し元は、整数が渡された場合にこれが機能することを期待します。 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.
};

対:

Magician.pullRabbitFromAir = fromAir() {
    return new Rabbit(); // out of thin air
}
Magician.pullRabbitFromStr = fromString(str)) {
    // more
}
Magician.pullRabbitFromInt = fromInt(int)) {
    // more
};

OK、それで私はあなたのコードに例外を追加しました(私はこれを強くお勧めします)、あなたが彼らがしたことをあなたに渡すとは想像もしなかったことを発信者に伝えます。しかし、特定のメソッドを作成する(JavaScriptでこれを実行できるかどうかはわかりません)と、コードが不要になり、呼び出し元として理解しやすくなります。このコードの作成者が考えたことについて現実的な仮定を設定し、コードを使いやすくします。

0
GlenPeterson