書こうとしたきっかけ
調べてもあまりMonkeypatching関連の初心者向けの記事が出なかったので、自分なりに勉強したのでメモがてらに記事にします。
筆者が一番触れてきた言語はtypescriptでもあるため、変なtype定義ではなく、仕組みを理解する点に着目してjavascriptで解説させていただきます。
間違ったところがありましたらご指摘していらだければ幸いです。では参りましょう。
題材
筆者自身はreduxのmiddlewareのドキュメントを読んでmonkeypatchingと言う名詞知ったのですが、その前では全く知らなかったですね...(勉強不足、ゼロからやり直してきます
redux知らない読者さんも沢山見てくれるはずなので、実際に解説に使うコードはreduxに触れないので、redux知らない人も安心してください。
Monkeypatchingは一体何なの?
モンキーパッチ(Monkey patch)は、システムソフトウェアを補完するために、プログラムをその時その場の実行範囲内で拡張または修正するというテクニックである。
wikiより。簡単に言うとソースコードを変更せずに実行時の動作だけ変えるものです。
まあ、筆者も見ただけで何言ってんのか全然分かってないので、実際にコードを書きながら理解しましょう(
解説
まずソースコードもどきを作成します。ライブラリー感覚のほうが理解しやすいので、export
してあげましょう。
// src-code.js
export default const _origin = {
log: (msg) => console.log("--> " + msg + " <--")
}
console.log()
をラップしただけのものになります。名前の_
は特殊の意味は何でもなく、ただ”外部ライブラリー”として他の関数と違う見た目の方が見やすいと言う感覚でつけました。
_origin.log("何かのメッセージ")
を呼べば元のconsole.log()
が呼ばれて、加工されたメッセージが出力される簡単なものです。
"外部ライブラリー"を導入
ここで先作ったソースコードもどきをimportして、ちゃんと予想通りの動作をしているかどうかを試しましょう。
import _origin from "./src-code.js"
_origin.log("Hello, world!") // この呼び出しのコードに注目しながら読んでほしい! 後で詳しく解説します!
これをコードを動作させるとちゃんとコンソールに--> Hello, world! <--
が出力されるはずです。
Let's Monkeypatch
ここれもしこの”外部ライブラリー”の動作が気に食わない、何か新機能を追加したい、もしくはbugを発見したので、updateを待たずに動作を変更させたい!
この場合、一番早いのは当然ソースコードを直接いじることなんですが、”外部ライブラリー”なので、ソースコードを弄れない!どうしよう!
そうだ、ソースコードを読んで、動作を再現したまま直接元のメソッドをオーバーライドしよう!
import _origin from "./src-code.js"
_origin.log = (msg) => {
console.log("before")
console.log("--> " + msg + " <--") // 元々の動作を再現する
console.log("after")
}
_origin.log("Hello, world!") // このコードに注目
// 出力:
// before
// --> Hello, world! <--
// after
ここでポイントになるのは、呼び出しのコードを変更していない点です。呼び出しの部分だけ見れば、まだ元のメソッドを呼び出しているようですよね。でも実際の動作はすでに変わっています。
このコードを動かせば、恐らく上のコメント通りのメッセージが出力されるでしょう。でも面倒ですよね、元々の動作を再現するコードをわざわざ自分で書かないといけないので、簡単な場合はいいですが、難しいものの場合、もしどこか間違って元通りでなくなったら大惨事ですね。
なのでこうしましょう
import _origin from "./src-code.js"
const _log = _origin.log;
_origin.log = (msg) => {
console.log("before");
_log(msg);
console.log("after");
};
_origin.log("Hello, world!");
// 出力:
// before
// --> Hello, world! <--
// after
これで先と同じ出力を得られつつ、具体的に元の関数の中に何の操作をやっているのかについて知らなくでも良いようになったんですよね。求められた引数を渡すだけで済むので。
これが一番簡単なMonkeypatchingになりますが、これだけだと結構不便ですね、もしあるところで元の動作をして欲しくて、他のところで改変した動作をして欲しい場合はどうしましょう。
そうだ、関数にまとめてあげましょう!
import _origin from "./src-code.js"
// パッチを適用させたい origin を引数として取る
const monkeyPatchOrigin = (origin) => {
const _log = origin.log;
origin.log = (msg) => {
console.log("before");
_log(msg);
console.log("after");
};
};
_origin.log("こんにちは、世界!")
monkeyPatchOrigin(_origin); // 関数を呼んで実際にパッチを適用させる
_origin.log("Hello, world!");
// 出力:
// --> こんにちわ、世界! <--
// before
// --> Hello, world! <--
// after
これでpatch
を呼び出す前に元の動作をし、patch
を適用したら新たな動作をしてくれるようになりました。よかったね!
ではここでさらに2個目のpatchを適用させたい場合はどう書けばいいかについて考えていきましょう。前の経験を踏まえるとこう書くはず!
import _origin from "./src-code.js"
const monkeyPatchOrigin = (origin) => {
const _log = origin.log;
origin.log = (msg) => {
console.log("before");
_log(msg);
console.log("after");
};
};
const monkeyPatchOriginTwo = (origin) => {
const _log = origin.log;
origin.log = (msg) => {
console.log("before-two");
_log(msg);
console.log("after-two");
};
};
_origin.log("こんにちは、世界!");
monkeyPatchOrigin(_origin); // 関数を呼んで実際にパッチを適用させる
monkeyPatchOriginTwo(_origin);
_origin.log("Hello, world!");
// 出力:
// --> こんにちは、世界! <--
// before-two
// before
// --> Hello, world! <--
// after
// after-two
あれ、なんか思ったのと違うですよね、2番目に適用しているpatchが先に出力されていますね。細かく処理を追って、なぜこのようになったのかを見ていきましょう!
まず最初に呼ばれるのは_origin.log("こんにちは、世界!")
で、patchが適用されていないので、このように一番出力されてるのが普通ですね。
次を見ていきくと、1つ目のpatchが適用されて、つまりこの時_origin.log
はどうなっているかというと
origin.log = (msg) => {
console.log("before");
_log(msg); // つまり console.log("--> " + msg + " <--")
console.log("after");
};
になっているはずですね、そして2番目のpatchが適用されるとどうなるかと言うと
origin.log = (msg) => {
console.log("before-two");
_log(msg); // ここはどうなるんだ...?
console.log("after-two");
}
2回目呼んでるときは1番目の後ろなので、つまり_log
が1回目の処理の時にオーバーライドされたものになるのですね。具体的にかくと
origin.log = (msg) => {
console.log("before-two");
console.log("before"); // 1回目の処理の結果をそのまま展開
console.log("--> " + msg + " <--");
console.log("after"); // ここまでが1回目の処理の結果
console.log("after-two");
}
ここまでやると多分お分かりになっていただいたと思います。順番の出力を得たい場合は、逆の順番にpatchを適用させないといけないですね。では逆に適用して、ちゃんと出力が変わったかを見て見ましょう。
そして、呼び出しに順番はあるのなら、_log
と言う訳の分からない名前より、もっとわかりやすいnext
に名前を変えましょう!
import _origin from "./src-code.js"
const monkeyPatchOrigin = (origin) => {
const next = origin.log;
origin.log = (msg) => {
console.log("before");
next(msg);
console.log("after");
};
};
const monkeyPatchOriginTwo = (origin) => {
const next = origin.log;
origin.log = (msg) => {
console.log("before-two");
next(msg);
console.log("after-two");
};
};
_origin.log("こんにちは、世界!");
monkeyPatchOriginTwo(_origin); // patchTwo を先に適用させる
monkeyPatchOrigin(_origin);
_origin.log("Hello, world!");
// 出力:
// --> こんにちは、世界! <--
// before
// before-two
// --> Hello, world! <--
// after-two
// after
逆に適用させると、ちゃんと出力も変わるはずです!
進撃!Middleware
今のコードを振り返ると、毎回適用するとき1個1個呼び出しているので、プログラミングらしくないですよね。何とかこのpatchの適用をまとめたいですね。
import _origin from "./src-code.js";
const monkeyPatchOrigin = (origin) => {
const next = origin.log;
origin.log = (msg) => {
console.log("before");
next(msg);
console.log("after");
};
};
const monkeyPatchOriginTwo = (origin) => {
const next = origin.log;
origin.log = (msg) => {
console.log("before-two");
next(msg);
console.log("after-two");
};
};
const applyPatch = (origin, patches) => {
patches.reverse(); // 順番を逆にする
patches.forEach((patch) => {
patch(origin);
});
};
_origin.log("こんにちは、世界!");
// 適用するときは普通に適用してOK
applyPatch(_origin, [monkeyPatchOrigin, monkeyPatchOriginTwo]);
_origin.log("Hello, world!");
// 出力:
// --> こんにちは、世界! <--
// before
// before-two
// --> Hello, world! <--
// after-two
// after
おめでとう!middlewareの原理を砕いで説明するとこういうことです。難しくはないでしょう。
ここまで来たらほぼ完成のようなもんですが、(実を言うとこれだけの機能ならここまで書けば良いんだと私は思うのですが、さらに処理をまとめると可読性が一気に下がってしまうので)、でもやはりポピュラーの書き方に沿ってこれら処理をまとめていきましょう。
ではまず1つにまとめるのは、origin.log = () => {}
の処理は複数の関数に渡って内部で行われているので、これをまとめましょう
import _origin from "./src-code.js";
// 関数名を middleware ぽく変更
const middlewareOne = (origin) => {
const next = origin.log;
// 内部でやらせないようにオーバーライドの関数を return してあげる
return (msg) => {
console.log("before");
next(msg);
console.log("after");
};
};
const middlewareTwo = (origin) => {
const next = origin.log;
// 同じく
return (msg) => {
console.log("before-two");
next(msg);
console.log("after-two");
};
};
//
const applyMiddlewares = (origin, middlewares) => {
middlewares.reverse();
middlewares.forEach((middleware) => {
// オーバーライドの処理はこちらに移動
origin.log = middleware(origin);
});
};
_origin.log("こんにちは、世界!");
applyPatch(_origin, [middlewareOne, middlewareTwo]);
_origin.log("Hello, world!");
ここまでやると、大分middlewareらしくなってきました。が、middleware関数の中にはまだ毎回const next = origin.log
と言う他の人から見ればなぜこの1行を書かないといけないのかが分からないコードが残っているので、これも修正しましょう。
import _origin from "./src-code.js";
const middlewareOne = () => {
return (next) => {
return (msg) => {
console.log("before");
next(msg);
console.log("after");
};
};
};
const middlewareTwo = () => {
return (next) => {
return (msg) => {
console.log("before-two");
next(msg);
console.log("after-two");
};
};
};
const applyMiddleware = (origin, middlewares) => {
middlewares.reverse();
middlewares.forEach((middleware) => {
origin.log = middleware()(o.log); // 2回returnしたので、呼び出しも2回にしないといけない
});
};
_origin.log("こんにちわ、世界!");
applyMiddleware(_origin, [middlewareOne, middlewareTwo]);
_origin.log("Hello, world!");
ここまできたら本当の本当に完成ですが、2回return
するのがあまり目に優しくないので、これもアロー関数特有の機能を使って修正しましょう。
import _origin from "./src-code.js";
// アロー関数の連続 return, 初めて見る人は何を書いているのかが分からないかもしれないが
const middlewareOne = () => (next) => (msg) => {
console.log("before");
next(msg);
console.log("after");
};
const middlewareTwo = () => (next) => (msg) => {
console.log("before-two");
next(msg);
console.log("after-two");
};
const applyMiddleware = (origin, middlewares) => {
middlewares.reverse();
middlewares.forEach((middleware) => {
origin.log = middleware()(origin.log);
});
};
_origin.log("こんにちは、世界!");
applyMiddleware(_origin, [middlewareOne, middlewareTwo]);
_origin.log("Hello, world!");
おめでとうございます!これで本当に完成です。目には優しくなったが、脳には優しくなくなった気もしなくはないですが、ともあれ、reduxのmiddlewareはこの見た目をしています。
終わりに
長かったですね、ここまで長くなるとは思ってなかったです。初心者にも優しいように結構細かく説明したつもりですが、最後のへんはちょっと飛ばしてた気がします((
でもまあ、最後はコードの整理だけしているので、言語ことに書きがたも違いますので、原理が理解できたらおっけーです👍