LoginSignup
50
32

More than 5 years have passed since last update.

TypeScriptのMethod Decoratorを解説する。

Posted at

好きな構文はアノテーションです。
メソッドを装飾して、そのメソッドが何に使われるかを定義する文法が大好きです。

各言語のアノテーションの説明

アノテーションはいろいろな言語で実装されていますが、それぞれ呼び方が違います。(この表現方法が正しいかはわかりませんが)
アノテーションはJavaの呼び方です。

Javaの例
@GetMapping
public String sayHello() {
   return "Hello World";
}

ScalaやKotlinなど、他のJVM言語でも同じような感じです。

C#だと属性

C#の例
[HttpGet]
public IActionResult SayHello(int id)
{
  return Content("Hello World");
}

Golangだと残念ながらメソッドにアノテーションはつけられません。
強いて言えば、構造体のパラメータにタグを付けることができます。

Goの例
type Model struct {
    Id        int64 `gorm:"primary_key"`
}

そしてTypeScriptではDecoratorといいます。
JavaScriptでは同様の仕組みがドラフトになっています。

TypeScriptの例
@Get()
sayHello() {
  return 'This action returns all cats';
}

構文は似ていますが、定義方法や動きは結構違います。Javaだとインターフェースのように実装しますが、TypeScriptだとfunctionです。
本書ではTypeScriptのDecoratorについて掘り下げます。
なぜTypeScriptかというと、私が今イチオシの言語だからです。

TypeScriptのDecoratorの定義方法

TypeScriptのDecoratorは以下のように実装できます。

MyDecorator.ts
export function MyDecorator(arg: string) {
  console.log("Decoratorが使われたときに呼ばれます。");
  return (target: any, name: string, descriptor: PropertyDescriptor) => {
    console.log("Decoratorをつけたメソッドが定義されたときに呼ばれます。");
    const method = descriptor.value;//もともとのメソッドを退避しておきます。
    descriptor.value = function() {
      console.log("メソッド実行前に呼ばれます。");
      //こんなふうに、アノテーションを付けた元のメソッドを実行
      const ret = method.apply(this, arguments);
      console.log("メソッド実行後に呼ばれます");
      return arg + ", " + ret;
    }
  }
}

で、このDecoratorを使います。

index.ts
import {MyDecorator} from "./MyDecorator";
class Person {
  name: string;
  public constructor(name: string){
    this.name = name;
  }
  @MyDecorator("example is prefix")
  public hello(message: string) : string {
    console.log("実メソッドが呼ばれます。");
    return message + "? hello " + this.name;
  }
}

console.log("クラスのインスタンス化");
const me = new Person("my name");
console.log("実メソッド呼び出し前");
const response = me.hello("morning");
console.log("処理完了 "+response);

これを実行すると、以下のように出力されます。

Decoratorが使われたときに呼ばれます。
Decoratorをつけたメソッドが定義されたときに呼ばれます。
クラスのインスタンス化
実メソッド呼び出し前
メソッド実行前に呼ばれます。
実メソッドが呼ばれます。
メソッド実行後に呼ばれます
処理完了 example is prefix, morning? hello my name

メソッド定義時と実行前後でDecoratorが呼ばれ、もともとのメソッドの戻り値が変更されているのがわかります。

使い方

メソッド実行前後に処理を挟めるため、log等に使えるのは想像に易いですね。
ここでは別の使い方をしてみます。Decoratorをつけたメソッドがロードされるときに処理を挟めるので、Decoratorをつけたメソッドを特定のイベントに関連付けて呼び出すようなコードを書いてみます。

まずはListenerというDecoratorを定義します。

Listener.ts
const map: { [key:string]:PropertyDescriptor[] } = {};
export function Listener(key: string) {
  console.log("Listener initialized ["+key+"]");
  return (target: any, name: string, descriptor: PropertyDescriptor) => {
    console.log("registed " + key);
    if(!map[key]) {
      map[key] = [];
    }
    map[key].push(descriptor);//メソッドを登録
  }
}
export function exec(key: string, value: string) {
   map[key].forEach(descriptor => descriptor.value.apply(descriptor, [value]));//指定メソッドを実行
}

ListenerはイベントをハンドリングするためのDescriptorです。

イベントを受け取るクラスを2つ用意します。

Class1.ts
import {Listener} from "./Listener"
export class Class1 {
   @Listener("event1")
   public method(message: string){
      console.log("Class1 method called " + message);
   }
   @Listener("event1")
   public anotherMethod(message: string){
      console.log("Class1 anotherMethod called " + message);
   }
}

もう一つ。

Class2.ts
import {Listener} from "./Listener"
export class Class2 {
   @Listener("event1")
   public method(message :string){
     console.log("Class2 method called " + message);
   }
   @Listener("event2")
   public anotherMethod(message :string){
     console.log("Class2 anotherMethod called " + message);
   }
}

あとは、アプリケーションを起動し、イベントを発行するスクリプト。

index.ts
import {Class1} from "./Class1";
import {Class2} from "./Class2";
import {exec} from "./Listener";

[Class1, Class2];

exec("event1", "message event1");
exec("event2", "message event2");

index.tsはClass1、Class2の存在は知っていますが、それぞれがどのイベントをハンドリングするかは知りません。
なにをどうハンドリングするのかを知っているのはClass1、Class2です。これでイベントハンドリングのロジックのカプセル化ができています。

index.tsを実行すると、以下のような出力がされます。

Listener initialized [event1]
registed event1
Listener initialized [event1]
registed event1
Listener initialized [event1]
registed event1
Listener initialized [event2]
registed event2
Class1 method called message event1
Class1 anotherMethod called message event1
Class2 method called message event1
Class2 anotherMethod called message event2

ちゃんとClass1、Class2がイベントを受け取れていますね。
めでたしめでたし。

ホントは、、、、

ホントは、index.tsはClass1、Class2の存在も知らずに、ただClass1が@Listenerを持っているメソッドがあれば、イベントを受け取れるように作りたかったのでしたが、、、、TypeScriptではやり方がわかりませんでした。TypeScriptは、クラスが解釈され、Decoratorが実行されるのが、(importでもなく)Classを処理中に初めて参照したとき、つまりは[Class1, Class2]のときなのです。
Javaであれば、アノテーションがついたクラスを探し出すことができるのですが、、、、もしTypeScriptでDecoratorがついたクラスを探し出す方法を御存知であれば、教えてください。

50
32
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
32