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


14

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

彼女は、そのユーザーがウサギを引き出したいことを知っているStringNumberFunction、多分HTMLElement。それを念頭に置いて、彼女は次のようにAPIを設計できます。

厳格なインターフェース

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

上記の例の各関数は、関数名/パラメーター名で指定された型の引数を処理する方法を知っています。

または、彼女はそれを次のように設計できます:

「アドホック」インターフェース

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

pullRabbitanything引数が想定されるさまざまな異なるタイプ、および(もちろん)予期しないタイプを考慮する必要があります。

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コンシューマーが渡すのに便利だと判断した引数で「機能する」という点で、外部から見る方が簡単だと感じています。

この質問への答えについては、どちらかのアプローチ(または理想的でない場合はまったく異なるアプローチ)に対する特定の長所と短所を見たいと思います。


回答:


7

いくつかの長所と短所

多態性の長所:

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

アドホックの長所:

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

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

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

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

  • オブジェクトを上昇させ、オブジェクトを「プル可能」にし、実行時にインターフェースに適合させ、次にマジシャンにプル可能オブジェクトを処理させます。
  • 戦略パターンを使用して、異なるタイプのオブジェクトの処理方法をマジシャンに動的に教えます。

解決策1:オブジェクトの上昇

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

つまり、何らかのタイプのオブジェクトを取得し、そのために帽子を引っ張る機能を追加します。何かのようなもの:

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

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

その場合、マジシャンは次のことができます。

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パターンの使用を提案しました。

これにより、実行時にさまざまなオブジェクトからウサギを引き抜く機能を追加し、非常に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に対してこれを行うのは悪い形式と広く見なされています。私も個人的には機能を避けています。一部の人はネイティブコンストラクターのプロトタイプに触れることに不安を感じるかもしれませんが、これは悪いポリシーではないかもしれませんが、この例は独自のオブジェクトコンストラクターで作業する場合に役立ちます。

それほど複雑ではないアプリで別のライブラリから何かを壊すような特定の用途のメソッドについては、このアプローチについては心配しませんが、JavaScriptのネイティブメソッド全体で過度に一般的なものをアサートしないようにするのは良い本能です古いブラウザで新しいメソッドを正規化する場合を除きます。

幸いなことに、メソッドまたはコンストラクター名をメソッドにいつでも事前マップできます(<object> .constructor.nameを持たないIE <= 8に注意してください。コンストラクタープロパティのtoString結果から解析する必要があります)。あなたはまだコンストラクタ名をチェックしています(オブジェクトを比較するときにJSではtypeofは一種の役に立たない)が、少なくともそれは巨大な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#のバックグラウンドから来て、これは私の開発スタイルです。1つのpullRabbitリクエストを作成し、その1つのリクエストタイプに渡された引数をチェックして、何かをさせることができます。これは、一度にどのタイプの引数が渡されるかを心配する必要がないことを意味します。厳密なアプローチを使用する場合、変数がどの型であるかを確認する必要がありますが、代わりにメソッド呼び出しを行う前にそれを行います。最後に質問は、電話をかける前に電話をかけるか、電話をかけた後に型を確認するかです。

これがお役に立てば幸いです。この回答に関連してさらに質問をしてください。自分の立場を明確にするために最善を尽くします。


0

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

わかりましたので、コードに例外を追加し(これを強くお勧めします)、呼び出し元に、彼らが何をするのか想像もしなかったことを伝えました。ただし、特定のメソッド(JavaScriptでこれが可能かどうかはわかりません)を記述することは、もはやコードではなく、呼び出し元として理解するのがはるかに簡単です。このコードの作者が考えたことについて現実的な仮定を設定し、コードを使いやすくします。


JavaScriptでできることを知らせてください:)
ベンジャミングリュンバウム

ハイパー明示が読みやすく/理解しやすい場合、ハウツー本はリーガルのように読めるでしょう。また、ほぼ同じことをすべて行うタイプごとのメソッドは、典型的なJS開発者にとって大きなDRYファウルです。タイプではなく、インテントの名前。必要な引数は、コード内の1つの場所、またはドキュメント内の1つのメソッド名で受け入れられた引数のリストを確認することで、明白または非常に簡単に検索できます。
エリックReppen
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.