これは何?
ふと,TypeScriptを書いていて,デコレータとコールバック関数って微妙に似ているなと思った。
デコレータもコールバック関数はどちらも関数を操作/制御する仕組みであり,同じようなことができそうだなと。
試しにある関数(ここではhello()
)の前後で処理を追加することを試してみたが,どちらを使っても同じことができる。
# デコレータ
cd /app && tsc && node dist/decorator.js
start
Hello, World!
end
デコレータの実装例
/**
* とりあえずのデコレータ
*/
function logging() {
return function(
originalMethod: any,
context: ClassMethodDecoratorContext<any, any>
) {
function replacementMethod(this: any, ...args: any[]) {
console.log("start");
const result = originalMethod.apply(this, args);
console.log("end");
return result;
}
return replacementMethod;
};
}
/**
* デコレータの使用するクラス
*/
class Greeter {
/**
* デコレータを使用するメソッド
* @param name 名前
*/
@logging()
hello(name: string) {
console.log(`Hello, ${name}!`);
}
}
// インスタンスを作成して実行
const greeter = new Greeter();
greeter.hello("World");
# コールバック関数
cd /app && tsc && node dist/callback.js
start
Hello, World!
end
コールバック関数の使用例
/**
* とりあえず,コールバック関数を試すクラス
*/
class Greeter {
/**
* コールバック関数を受け取るメソッド
*/
static logger(callback: (greeter: Greeter) => void) {
const greeter = new Greeter();
console.log("start");
callback(greeter);
console.log("end");
}
/**
* @param name 挨拶する相手の名前
*/
hello(name: string) {
console.log(`Hello, ${name}!`);
}
}
Greeter.logger((greeter: Greeter) => {
greeter.hello("World");
});
そのため,デコレータとコールバック関数について
- 概要
- どのような時に使われるか
- デコレータとコールバック関数どちらを使うべきなのか
をまとめてみた。
詳しい方がいればコメント欄等に意見をいただけると大変喜びます。
デコレータについて
デコレータの概要
デコレータというと自分の中ではPythonのイメージが強いので,Pythonの公式ドキュメントを引用した。
decorator
(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:
def f(arg): ... f = staticmethod(f)
@staticmethod def f(arg):
シンタックスシュガーとは
糖衣構文(とういこうぶん、英: syntactic sugar あるいは syntax sugar)は、プログラミング言語において、読み書きのしやすさのために導入される書き方であり、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののことである。 2
上の例について少し補足すると,@staticmethod
をf
関数につけること=関数を引数にとる関数staticmethod()
の引数にf
関数を渡し,f
関数の定義を上書きしている。
つまり,f("hoge")
を実行する場合を考えると,f = staticmethod(f)
と定義しているので,
f("hoge")
= staticmethod(f)("hoge")
になる。
念の為,staticmethod
の実装はどうなっているか確認しておく。
staticmethod
は,通常の関数をstaticmethodにして返しているため,34
f("hoge")
= staticmethod(f)("hoge")
= 変換されたメソッド("hoge")
のようになる。
簡単にまとめると,「関数を引数にとる関数を使用することで,もとの関数を上書きする」というような動きをしている。
冒頭に記載したデコレータのサンプルコードについて
冒頭にも記載したコードにおいての,デコレータの動きを確認しておく。
/**
* とりあえずのデコレータ
*/
function logging() {
return function(
originalMethod: any,
context: ClassMethodDecoratorContext<any, any>
) {
function replacementMethod(this: any, ...args: any[]) {
console.log("start");
const result = originalMethod.apply(this, args);
console.log("end");
return result;
}
return replacementMethod;
};
}
/**
* デコレータの使用するクラス
*/
class Greeter {
/**
* デコレータを使用するメソッド
* @param name 名前
*/
@logging()
hello(name: string) {
console.log(`Hello, ${name}!`);
}
}
// インスタンスを作成して実行
const greeter = new Greeter();
greeter.hello("World");
試しにgreeter.hello
をconsole.logで出力してみると,replacementMethod
であることがわかる。
cd /app && tsc && node dist/decorator.js
[Function: replacementMethod]
つまり,greeter.hello("World")
を実行するとlogging
で定義したreplacementMethod
が実行され,もとのメソッドの実行前に処理を追加することができている。
デコレータの使うタイミングはいつか?
TypeScriptのドキュメント5を見ると,デコレータは既存のクラスやメソッドに対して要素を追加するための方法として存在しているようだ。
Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.
meta-programingとは
メタプログラミング (英語: metaprogramming) [注釈 1]とはプログラミング技法の一種で、一般に「プログラムを記述するプログラム」を書くこと、またはそのプログラムを指す[1]。対象言語に埋め込まれたマクロ言語によって行われることもある。 6
一般的なデコレータを使割れるケースとしては,以下のようなケースで使われているイメージがあるが,
- ロギング: メソッドの呼び出しや終了時にログを出力する
- バリデーション: メソッドの引数のバリデーションチェックを行う
- 認可,アクセス制御: API使用前にトークンを検証する
主体はデコレートされるメソッドにある。
コールバック関数について
コールバック関数の概要
コールバック関数は、引数として他の関数に渡され、外側の関数の中で呼び出されて、何らかのルーチンやアクションを完了させる関数のことです。
コールバックベースの API の利用者は、API に渡す関数を書きます。API の提供者(caller と呼ばれる)は関数を受け取り、呼び出し側の本体内のある時点で関数をコールバック(実行)します。呼び出し側はコールバック関数に正しい引数を渡す責任があります。また、呼び出し側はコールバック関数からの特定のな返値を期待することがあり、呼び出し側のさらなる動作を指示するために使用します。
7
つまり,コールバック関数は「他の関数に引数として渡される」関数である。
冒頭に記載したコールバック関数のサンプルコードについて
/**
* とりあえず,コールバック関数を試すクラス
*/
class Greeter {
/**
* コールバック関数を受け取るメソッド
*/
static logger(callback: (greeter: Greeter) => void) {
const greeter = new Greeter();
console.log("start");
callback(greeter);
console.log("end");
}
/**
* @param name 挨拶する相手の名前
*/
hello(name: string) {
console.log(`Hello, ${name}!`);
}
}
Greeter.logger((greeter: Greeter) => {
greeter.hello("World");
});
※これはデコレータ本来の使い方では無い気がする。将来は考察を参照
試しに,static methodにしてみた。
(デコレータのコードと対称でないが,まあ一旦いいやとしている)
やっている処理としては,Greeter
クラスのstatic methodであるlogger()
を呼び出し,そのコールバック関数としてhello
を実行している。
コールバック関数を使うタイミングはいつか?
非同期処理やイベントハンドラ
よく見るのは,イベントハンドラー等で使うケースである。
// ボタンクリック時に実行する処理を定義
button.addEventListener("click", (event) => {
console.log("button is clicked!");
});
// MCP Server側で,MCP Clientからの使用可能なツールの一覧を取得するリクエストに対するリクエストハンドラ
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_or_update_file",
description: "Create or update a single file in a GitHub repository",
inputSchema: zodToJsonSchema(files.CreateOrUpdateFileSchema),
},
...
依存性注入
class McpClient {
static async session(callback: (client: McpClient) => Promise<void>) {
const client = new McpClient()
try {
await callback(client)
} catch (error: any) {
console.log("error");
} finally {
await client.close() // セッションや接続の後始末
}
}
...
}
await McpClient.session(async (client) => {
await client.doSomething() // 後始末やエラーハンドリングを気にしなくて良い
})
- 呼び出し側が
close()
を呼び出し忘れることを防げる - 呼び出し側が
.doSomething()
の変わりに別の関数を渡したり,追加したりできるなどカスタマイズ性が高い// session()が使用する関数をハードコーディングする例 class McpClient { static async session() { const client = new McpClient() try { await client.doSomething(); // カスタマイズができない } catch (error: any) { console.log("error"); } finally { await client.close() // セッションや接続の後始末 } }
(配列操作など)
競技プログラミングとかで便利そう?
const testList = [1, 2, 3].map(x => x * 2); // [2, 4, 6]
考察: 結局,デコレータとコールバック関数の違いはなにか
いろいろ調べた結果の自分の見解だが,それぞれが対象とする領域は異なると思う。
デコレータを選択する時
デコレータは実装の詳細を共通化したい時に使う。観察可能な振る舞い(observable behavior)つまり,メインのロジックを書く時には使わない。
- あくまで,関数やクラスに処理を付け加える(修飾する)もの。処理の主体はデコレートされる関数にあり,デコレータではない。
- デコレータが対象とする領域はロギング,バリデーション,認可/アクセス制御といったボイラープレート的なコードが多そう。
- デコレータ側に共通処理が書くことで,コードの再利用性を高める
コールバック関数を選択するとき
逆に,コールバック関数は観察可能な振る舞いを変化させるのに使う。
- 引数として関数を渡すのがコールバック関数なので,観察可能な振る舞いが変化する。コールバック関数側も処理の主体である
- コールバック関数によって観察可能な振る舞いを変化させることができるので,コールバックを引数に取る関数側の再利用性を高める
まとめると,デコレータを使うべきなのか,それともコールバック関数を使うべきかに迷った際には,実装の詳細を書きたいのか,それとも観察可能な振る舞いを書きたいのかをもとに判断すると良いのではないだろうか。