見出し画像

デコレータ複数定義の罠~ミスを学びに変えよう~



😀初めに

こんにちは。株式会社ラキール DX Productグループ所属の吉澤です。
僕のグループはLaKeel DX基盤を用いたアプリケーション開発に取り組んでいます。
僕はHR人事チームに所属しており、LaKeel HRの開発に携わっています。
LaKeel HRはLaKeel製品群の中で若い製品であり、絶賛売り出し中、毎日刺激的で学びの多い日々を過ごしています。

さて、少し時期は戻りますが、2023年3月16日にTypeScript 5.0が公式リリースされました。TypeScript 5.0の大きな変更点として、デコレータ機能への正式対応がありました。
今回はこのホットなデコレータ機能で僕が犯したミスと原因、解決についてお話ししたいと思います。
※本記事ではTypeScript 3.9を使用しており、stage2のデコレータ機能を使用しています。

本題に入る前に、TypeScriptのデコレータ機能に関して説明させていただきます。

❓デコレータとは

クラス、メソッド、アクセサ、プロパティ、パラメータに対してカスタムな振る舞いを追加できる機能です。
デコレータを使用することで、クラスやメソッドなどを修飾し、処理を動的に変更したり、メタデータを追加したりすることができます。
一般的な例としては、ログ出力、バリデーション、アクセス制御、性能計測などに使用されます。

デコレータの基本的な構文は以下になります。

// クラスデコレータの例

// クラスデコレータの定義
function myClassDecorator(constructor: Function) {
  console.log("Class is being decorated");
}

// クラスに適用する
@myClassDecorator
class MyClass {
  // Class implementation
}

// メソッドデコレータの例

// デコレータの定義
function myMethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Method is being decorated");
}

// メソッドに適用する
class MyClass {
  @myMethodDecorator
  myMethod() {
    // Method implementation
  }
}

修飾する対象の宣言の際に、対象の前に'@'を付けてデコレータを記述します。
引数や評価タイミングは、デコレータを適用する対象のタイプによって変化します。

デコレータ複数定義の評価順

一つの対象に対して複数のデコレータを定義した場合は下から上に評価されます。

// 1. decorator1デコレータを定義
function decorator1(constructor: Function) {
  console.log('firstDecorator called.')
}

// 2. decorator2デコレータを定義
function decorator2(constructor: Function) {
  console.log('secondDecorator called.')
}

// 3. decorator1とdecorator2をClassWithMultipleDecoratorsクラスに適用
@decorator1
@decorator2
class ClassWithMultipleDecorators {
}

// 4. 出力結果
secondDecorator called.
firstDecorator called.

❔デコレータファクトリとは

デコレータファクトリは、デコレータを返す関数のことを指します。
デコレータに対してパラメータを渡す場合や、デコレータを動的に生成する場合に使用されます。

デコレータファクトリの基本的な構文は以下になります。

// 1. myDecoratorFactoryデコレータファクトリを定義
function myDecoratorFactory(value: string) {
  // myDecoratorデコレータを返す
  return function myDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`デコレータが呼び出されました。デコレータファクトリへの引数は「${value}」でした。`);
  };
}

// 2. myMethod1とmyMethod2にデコレータファクトリを適用
class MyClass {
  @myDecoratorFactory("1つめ")
  myMethod1() {
    // Method implementation
  }

  @myDecoratorFactory("2つ目")
  myMethod2() {
    // Method implementation
  }
}

const myInstance = new MyClass();
MyClass.myMethod1();
MyClass.myMethod2(); 

// 3. 出力結果
デコレータが呼び出されました。デコレータファクトリへの引数は「1つめ」でした。
デコレータファクトリが呼び出されました。デコレータファクトリへの引数は「2つめ」でした。

このように、デコレータファクトリはデコレータに固定で渡される引数以外に、こちらで記述した引数を受け取ることができるため、すべてに一定の処理をするのではなく、対象ごとに処理を変化させる際等に使用されます。

デコレータファクトリ複数定義の評価順

デコレータファクトリの評価順ですが、「LIFO」(Last In First Out)です。
先に読み込んだデコレータファクトリのデコレータは後から実行されます。

こちらのドキュメントに記述があり、
例がわかりやすかったのでコメントを付け加えて説明します。

// 1. firstFactoryデコレータファクトリの宣言
function firstFactory() {
 console.log("firstFactory(): factory evaluated");
  // firstDecoratorデコレータを返す
  return function firstDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
   console.log("firstDecorator(): called");
  };
}

// 2. secondFactoryデコレータファクトリの宣言
function secondFactory() {
  console.log("secondFactory(): factory evaluated");
  // secondDecoratorデコレータを返す
  return function secondDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("secondDecorator(): called");
  };
}

// 3. デコレータファクトリを適用したクラスメソッドの宣言 
class ExampleClass {
  @firstFactory()
  @secondFactory()
  method() {
    // Method implementation
  }
}

// 4. メソッドの呼び出し

ExampleClass.method();

// 5. 出力結果
firstFactory(): factory evaluated
secondFactory(): factory evaluated
secondDecorator(): called
firstDecorator(): called

デコレータファクトリの読み込み順は上から下になるのですが、
返されたデコレータが実行される順番は下から上になります。
その為、コンソールには
「firstFactory(): factory evaluated」
→「secondFactory(): factory evaluated」
→「secondDecorator(): called」
→「firstDecorator(): called」
の順番で値が出力されます。

このように、デコレータファクトリの中での処理と、デコレータファクトリが返すデコレータの評価順は異なる順番となります。

🎭デコレータとデコレータファクトリの違い

ドキュメントや記事を呼んでいると、デコレータファクトリもデコレータと記述されており、違いがわかりづらいです。
デコレータ機能の中にデコレータファクトリがあるため、デコレータファクトリもデコレータと呼ばれているみたいです。
違いの見分け方としては、

  • デコレータ宣言時
    デコレータファクトリは関数(デコレータ)を返却し、
    デコレータは関数以外を返す、もしくは返り値がありません。

// デコレータ
function decorator(target: any, propertyKey: string) {
  console.log('デコレータが呼び出されました。');
}

// デコレータファクトリ
function decoratorFactory(arg: string) {
  return function decorator(target: any, propertyKey: string) {
    console.log(`デコレータファクトリが呼び出されました。 引数: ${arg}`);
  };
}
  • デコレータ適用時
    @decorator() と記述されていれはデコレータファクトリ、
    @decorator と記述されているのはデコレータです。

class ExampleClass {
  @decorator // デレータを使用
  prop1: string;

  @decoratorFactory() // デコレータファクトリを使用
  prop2: string;
}

で間違えないかなと思っています。

🐛デコレータのミスに関して

さて、本題です。
先日、APIの実装において、デコレータの記述でミスを犯してしまいました。
ミスの内容は、同じデコレータファクトリを二つ定義してしまうというものでした。

僕が書いたコードは以下になります。(名称等は変更しています。)

    @ApiMeta({
        retrySettings: {
            retry: true,
            options: {
                statusCodes: [429, 500, 502, 503, 504],
                logging: true
            }
        }
    })
    @ApiMeta({
        auth: false
    })
    @Put()
    async methodName(@Body() body: methodRequestBody, @Res() res: Response): Promise<methodResponse> {
        ....
    }

@ApiMeta()はLDXライブラリのデコレータファクトリであり、
このデコレータファクトリによってAPIの認証設定やリトライ設定、プライベート設定を管理しています。

僕は一つ目の@ApiMeta()にリトライ設定を記載し、二つ目の@ApiMeta()に認証設定を記載してしまいました。
@ApiMeta()へ渡したオプションは上書き反映される処理になっており、結果、認証設定は反映されませんでした。
評価順の説明を前でさせていただいたので認証設定が反映されなかった理由はお分かりになるかと思いますが、
初めに認証設定が反映され、その後リトライ設定が反映されたため、認証設定は上書きされてしまいました。

このAPIはLDXの認証情報を持たない外部サービスから実行されるAPIである為、認証不要の設定を入れなければ全ての処理を認証エラーで返してしまいます。気づかなければあわや大惨事の一歩手前でした。。

さて、ミスをしたら原因を突き止め、対策をしないとです。

💭原因

今回のミスの原因は、同一のデコレータファクトリを複数回記述してしまったことに気づけなかったことだと考えています。
レビューフローやテスト方式などの間接的な原因も考えられますが、根本的な原因はここにあると考えています。

デコレータは柔軟なカスタマイズが可能で、様々な処理を行わせることができます。
しかし、その分デコレータごとにルールが変わるため、テストが難しくなり、不具合が生じやすくもなります。
今回のケースでも静的テストや動的テストではエラーは検知されませんでした。

⛏今後の対策

このミスの対策として、@ApiMeta()の処理に、リトライ設定や権限設定の情報がすでに反映済であれば、エラーを投げるように修正しました。
(実際に修正してくれたのはSDKチームです)

これにより、CI/CDのジョブを回す際にエラーが出るようになり、複数定義に必ず気づけるようになりました。
SDKチームはとても頼りになります、、!
いつもありがとうございます。🙏🙏

👩‍🏫まとめ

さて、デコレータ機能の説明、デコレータに関してのミスに関して書いてきましたが、いかがだったでしょうか。
デコレータは共通の処理を使いまわすことのできる非常に強力な機能ですが、その一方で処理の意図を十分に理解せずに利用することで悲惨な結果を招くこともあります。
今回はデコレータで行っている処理の重大さや、デコレータ機能を理解できていなかった自分の無知さ故に起こってしまったミスでした。

皆さんの中でも、前任者の真似をして使いまわしている処理や、長らく触れずにいる処理があるのではないでしょうか。
処理を理解できていない、ということはこれからのコードの拡張性の低下や、バグの発生、パフォーマンス低下に繋がります。

僕のようなミスを防ぐためにも、単なる機械的なタイピングだけでなく、問題解決能力と洞察力を持って、プロフェッショナルなソフトウェアエンジニアとしてのステップを踏んでいきましょう!