このページの本文へ

JSのソースコードが変えられない!困ったときにモンキーパッチで逃げ切る方法

2016年10月06日 05時30分更新

文●Vildan Softic

  • この記事をはてなブックマークに追加
本文印刷
他人が作ったJavaScriptのプログラムを部分的に、でも元のコードを修正せずに対応するしかない…。そんなときにモンキーパッチ。やらなきゃいけないときの対処方法を実例で。

たった1つの小さな問題点を除いてきちんと動作する、第三者のコードを使ったことはありませんか? なぜ作成者はこんなやっかいなコンソールログを削除し忘れてしまったのだろう、そのAPIコールがもう1つちゃんとしていればうまくいったのに、と思うことがあるでしょう。こうした場合、メンテナンス担当者に変更箇所を実装させるのは困難(あるいは不可能)です。自分自身でコードを変更しようにも、ソースコードを持っていなかったり、自分でコードをホストしたくなかったりするときはどう対処すればよいか悩みます。

さあ、JavaScriptのモンキーパッチの世界を一緒に探検しましょう!

この記事ではモンキーパッチとはなにかを説明します。また、第三者が作ったウィジェット機能を要件に合うようにモンキーパッチで変更するいくつかの事例を紹介します。

モンキーパッチとは?

モンキーパッチ(以下「MP」)は、元のソースコードを変更せずにコードセグメントのデフォルト動作を拡張・抑制する、オーバーライドのテクニックの1つです。修正されたバージョンに元の動作を置き換えることによって実現します。

記事では既存のfeedback boxウィジェットを使用します。次のように表示される、フィードバック用のフォームを持ったシンプルでスライド可能なポップアップです。

A feedback form widget, which is going to be monkey patched

ソースコードはMPのターゲットとして動作するユースケースを含むように修正しています。ここでのターゲットとは、特定の機能や特徴、パッチを適用する対象となる最低限のメソッドを指します。

もう1つの修正点は、MPのテクニックに集中するためコード周辺の即時関数(Immediately-invoked function expression:IIFE)を削除したことです。

本記事で議論されているMPを含むサンプルの全体はPlunkerで参照できます。

モンキーパッチは悪習?

本題に入る前に1つはっきりさせておきましょう。MPは悪い習慣——不正なeval関数、命令型プログラミング、変更可能なデータ構造、双方向バインディングなど——だと捉えられています。

これらの方法を用いる場合、きっと間違ったやり方だということや、より良い状態になるようにあちこち変更すべきだとアドバイスしてくれるチームがいると思います。しかし、当然のことですが用途に応じてさまざまなツールやテクニックが存在します。極端で、ばかげていて、単なる使えないものに見えたとしても、場合によっては最終手段になり得るのです。しかし残念ながら、役立たないものだと見なされているため、解説する記事はあまり出回っていません。

この記事で説明するシチュエーションは、フェイクウィジェットを使った極端なものですが、どのような選択肢があるかを紹介することを目的としているので、不自然に思えるかもしれません。読者は、この記事で解説している方法が自分の好みに合うかどうか判断する必要があります。それでも、この記事を読み終えるころには、MPに異議を唱えられる程度には理解が深まるでしょう。

モンキーパッチの目標

テクニックの話に進む前に、実現したいことを分析します。修正されたウィジェットには対処すべきコードがいくつか含まれています。

ハードコーディングされた背景色

1つ目はtoggleErrorと呼ばれるメソッドで、booleanパラメーターに基づいて要素の背景色を変更します。

FeedbackBox.prototype.toggleError = function(obj, isError) {
  if(isError) {
    obj.css("background-color", "darkgrey");
  } else {
    obj.css("background-color", "");
  }
}

見ての通り、jQueryのCSSメソッドでbackground-colorプロパティを設定しています。本来はスタイルシートのルールを使って指定したいので、これは問題です。

やっかいなコンソールログ

ウィジェットの開発時には、現在実行している開発のヒントを得るのにコンソールログを利用しました。しかし、開発時に良いやり方だったことが必ずしも製品を作る際の最善の方法になるわけではありません。だからこそ、それらすべてのデバッグ文を削除する方法を見つける必要があります。

広告サーバーの呼び出しの遮断

このウィジェットは素晴らしいものですが、1つ奇妙な動作をします。スクリプトを初期化するたびに、おかしな広告サーバーに要求を出して、不要な大量の広告をページ上に表示します。

FeedbackBox.prototype.init = function() {
  // call to an adserver we'd like to skip
  $.ajax('vendor/service.json', {
    method: 'GET'
  }).then(function(data) {
    console.log("FeedbackBox: AdServer contacted");
  });

  ...

注意:このデモコードは外部へのAjaxの要求をシミュレーションするためにPlunker内部のJSONを対象にしています。何が言いたいかのかお分かりいただけうるとうれしいです。

メソッドの上書き

MPの主要な概念の1つは既存の関数を取得し、元のコードを呼び出す前後にカスタム動作で拡張するということです。しかし、元の実装の呼び出しはカスタム動作に置き換えたい場合だけでよく、常に必要ではありません。この方法はハードコーディングされた背景色の問題を解決するのにぴったりです。

MPを適用する場所は、元の実装がロードされ利用可能な状態になったあとでなければなりません。通常はなるべくターゲットの近くに変更を適用すべきですが、ターゲットの実装がそのうち変更される可能性があることに留意してください。例として、MPを使用した初期化処理はmain.jsに移動します。

ウィジェットの実装を見ると、ウィジェットのルートとして機能するFeedbackBoxオブジェクトが存在することが分かります。そのあとで、toggleError関数がプロトタイプに実装されています。

function FeedbackBox(elem, options) {
  this.options = options;  
  this.element = elem;  
  this.isOpen = false;
}

FeedbackBox.prototype.toggleError = function(obj, isError) {
  ...
}

JavaScriptは動的な言語であり、オブジェクトは実行時に修正されることもあるので、最終的には単にtoggleErrorをカスタムメソッドと置き換えれば良いだけです。その際、同じシグネチャ(名前および引数)を使い続けることだけは忘れないようにしてください。

FeedbackBox.prototype.toggleError = function(obj, isError) {
  if(isError) {
    obj.addClass("error");
  } else {
    obj.removeClass("error");
  }
};

これで新しい実装によって所定の要素にエラークラスが追加され、CSSで背景色を設定できるようになりました。

メソッドの拡張

前の例では、独自の実装を用いて元の実装を上書きする方法を見てきました。一方、コンソールログの管理というのは、本来は特定の呼び出しをフィルタリングしたり抑制したりするだけで良いはずです。成功の秘訣は埋め込んだコードを綿密に点検し、その処理の流れを理解するように努めることです。通常、好きなブラウザーの開発者コンソールを立ち上げて処理の流れを理解します。ロードされたリソースをピークして、ブレークポイントを追加し、ターゲットコードの1部をデバッグし、なにをしているのかの感触をつかみます。とは言っても、今回は別のタブにあるvendor/jquery.feedBackBox.jsという名称のPlunkerのサンプルから実装すれば良いだけです。

デバッグメッセージを見ると、それぞれ「FeedbackBox:」という文字列から始まっていることが分かります。つまり、やりたいことを達成する簡単な方法の1つは、元の呼び出しを遮断し、与えられたテキストが書き込まれているか検査し、デバッグヒントが含まれない場合にのみ元のメソッドを呼び出すことです。

そのためには、あとで使用できるように元のconsole.logを変数に保存してから、再び元の実装をカスタムの実装でオーバーライドします。得られたtext属性が文字列型かどうか確認し、文字列型であればFeedbackBox:という部分文字列が含まれるかどうかを確認します。含まれる場合は特になにもしませんが、含まれない場合はapplyメソッドを呼び出して元のコンソールコードを実行します。

このメソッドは第1パラメーターとしてコンテキストを取得し、さらにarguments変数を取得することに注意してください。コンテキストは呼び出されるべきメソッドのオブジェクトを指します。また、arguments変数は元のコンソールログ呼び出しに最初に渡された引数すべての配列です。

var originalConsoleLog = console.log;
console.log = function(text) {
  if (typeof text === "string" && text.indexOf("FeedbackBox:") === 0) {
    return;
  }

  originalConsoleLog.apply(console, arguments);
}

注意:単純にtext属性を転送しなかったことに疑問を感じるかもしれません。実際にはconsole.logは無限のパラメーターで呼び出し可能で、最終的には1つのテキスト出力に連結できます。無限に対応するのはとても困難なので、すべてを定義するのではなく、ただ来るものを全部転送します。

Ajaxの呼び出しの遮断

最後に広告サーバーの問題に対処する方法について説明します。もう一度ウィジェットのinit関数を見てください。

$.ajax({
  url: './vendor/a-d-server.json',
  method: 'GET',
  success: function(data) {
    console.log(data);
    console.log("FeedbackBox: AdServer contacted");
  }
});

最初に考えるのは、ブラウザーを立ち上げてjQueryプラグインを上書きする方法を検索することかもしれません。検索スキルの高さによっては、適切な答えが見つかるかもしれないし、見つからないかもしれません。しかし、一度立ち止まって実際になにが起こっているのか考えてみてください。たとえjQueryがajaxメソッドを使ってなにかをしたとしても、ある時点で最終的にネイティブのXMLHttpRequestオブジェクトを生成します。

内部でどのように動作しているか説明します。もっとも簡単な例はMDNにある以下のコードです。

var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
  if (xhttp.readyState == 4 && xhttp.status == 200) {
      // Action to be performed when the document is read;
  }
};
xhttp.open("GET", "filename", true);
xhttp.send();

新しいXMLHttpRequestのインスタンスが生成されているのが分かります。onreadystatechangeメソッドが保持されていますが、実際には気にしなくてよいでしょう。また、openおよびsendメソッドが存在します。この方法ではsendメソッドにMPを適用して、特定のURLへの呼び出しを実行しないように伝えます。

var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data) {
  if ( URL DOES NOT POINT TO AD SERVER ) {
    return originalSend.apply(this, arguments);
  }

  return false;
};

オブジェクト自身からターゲットのURLを取得できないことが分かります。それではどうすれば良いのでしょうか? オブジェクト上でURLを利用可能にすれば良いのです。URLを取得する最初のときに、openメソッドが第2パラメーターとしてURLを受け取ります。オブジェクト自身で利用可能なURLを生成するために、まずopenメソッドにMPを適用します。

先と同様に、あとで使えるように元のopenメソッドを変数内に保存します。それから、カスタム実装で元の実装を上書きします。動的な言語であるJavaScriptを使った作業を楽しみつつ、状況に応じて新しいプロパティを生成し、それを_urlとします。_urlは引数で渡された値をセットします。

var originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
  this._url = url;
  return originalOpen.apply(this, arguments);
};

元のopenメソッドを呼び出し、それ以上はなにもしません。

再びsendメソッドのMPに戻りますが、条件チェックのやり方がとても明確です。以下が更新されたバージョンです。

var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data) {
  if (this._url !== "./vendor/a-d-server.json") {
    return originalSend.apply(this, arguments);
  }

  return false;
};

最後に

記事では、MPで実行時のコードの動作を変更するための簡単な説明をしました。しかしそれ以上に、MPを使用して課題に取り組む方法について、みなさんの参考になることを願っています。たいていの場合パッチ自体がとてもシンプルなので、実行時にコードを調整する方法や場所を考えることはとても重要です。

また、MPについてどう感じたかにかかわらず、動的な言語による美しい動作を見る良い機会が得られたのではと期待しています。MPを使えば、実行時に元の実装さえも動的に変更できるようになります。

※本記事はMoritz KrögerTom Grecoが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Pragmatic Uses of Monkey Patching in JavaScript

[翻訳:市川千枝/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事