TypeScriptでデコレーターを使ってクラスメソッドにログ機能を追加する方法

TypeScriptのデコレーターは、クラスやメソッド、プロパティに対して追加の機能や処理を付与するための構文です。デコレーターを使用することで、コードの繰り返しを減らし、特定の機能を一箇所で集中管理することが可能です。例えば、クラスメソッドに対してログを自動的に記録する機能を付与することができ、コードのメンテナンス性や可読性を向上させる手段として非常に有用です。本記事では、TypeScriptでデコレーターを使い、クラスメソッドにログ機能を追加する方法を具体的に紹介していきます。

目次

デコレーターの導入手順

TypeScriptでデコレーターを使用するためには、まずプロジェクトにおいていくつかの設定を有効にする必要があります。デコレーターはECMAScriptの標準機能ではないため、TypeScriptのコンパイラオプションを調整してサポートを有効にします。以下に、デコレーターを使用するための基本的な手順を示します。

TypeScriptコンパイラの設定

tsconfig.jsonファイルを開き、次のオプションを設定します。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
  • experimentalDecorators: デコレーターを使用するために必要なオプションです。
  • emitDecoratorMetadata: クラスメタデータをデコレーターで利用できるようにします。

TypeScriptプロジェクトへのデコレーターの導入

次に、デコレーターが動作する環境を整えたら、デコレーターを適用したいクラスやメソッドにデコレーター関数を定義します。これにより、特定のクラスメソッドにログ機能を付与できる準備が整います。

これでデコレーターを利用するための準備は完了です。次に、実際にログ機能を追加する方法について説明していきます。

クラスメソッドにログ機能を追加するメリット

ログ機能をクラスメソッドに追加することは、コードの保守性やデバッグ作業を大幅に向上させる重要な手段です。デコレーターを使用してログ機能を追加することで、煩雑なログ出力処理をメソッド内部に埋め込む必要がなくなり、コードの見通しが良くなります。以下では、クラスメソッドにログ機能を付与する具体的なメリットを説明します。

メソッドの動作をトレースできる

ログを自動的に記録することで、メソッドがどのタイミングで呼び出され、どのような引数が渡されたのか、さらに返り値が何であるのかを容易に確認できます。これにより、プログラムの動作を詳細にトレースし、予期しない動作やバグを早期に発見することが可能です。

デバッグ作業の効率化

ログは、コード内のどこでエラーが発生したかを迅速に特定する手助けをします。特に複雑なアプリケーションでは、どのメソッドがエラーを引き起こしているかを見つけるのは困難ですが、ログを活用することで、メソッド単位でのトラブルシューティングがしやすくなります。

開発者間のコードの可視性向上

プロジェクトチーム内で作業している場合、ログは他の開発者がコードの動作を理解するのに役立ちます。ログ出力が統一されていれば、他の開発者がコードの動作状況を簡単に把握でき、コードレビューやデバッグの際のコミュニケーションもスムーズになります。

コードの保守性向上

デコレーターによってログ機能を一箇所で集中管理できるため、複数の場所に同様のログ記述を繰り返す必要がなくなります。これにより、コードのメンテナンスが容易になり、ログ仕様の変更も一箇所で行うだけで済むため、開発の効率が向上します。

ログ機能を追加することで、クラスメソッドの動作を可視化し、効率的なデバッグや保守が可能になります。次は、実際にメソッドデコレーターを作成する方法について見ていきます。

メソッドデコレーターの作成方法

TypeScriptでは、メソッドデコレーターを使ってクラスメソッドに特定の処理を追加することができます。今回は、クラスメソッドにログ機能を追加するためのデコレーターを作成します。このデコレーターによって、メソッドが呼び出された際に、その引数や戻り値を自動的にログ出力できるようになります。

メソッドデコレーターの基本構造

メソッドデコレーターは、ターゲットとなるメソッドに適用される関数であり、次のようなシグネチャを持ちます。

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  // オリジナルメソッドを保存
  const originalMethod = descriptor.value;

  // メソッドをラップしてログ処理を追加
  descriptor.value = function (...args: any[]) {
    console.log(`Method ${propertyKey} called with arguments: ${JSON.stringify(args)}`);

    // オリジナルのメソッドを実行
    const result = originalMethod.apply(this, args);

    console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
    return result;
  };

  return descriptor;
}

コードの説明

  • target: デコレーターが適用されるクラスのプロトタイプです。
  • propertyKey: デコレーターが適用されるメソッドの名前です。
  • descriptor: 対象メソッドのプロパティディスクリプターです。これによりメソッドの挙動を変更できます。

上記のデコレーターでは、メソッドのオリジナル実装を保存し、descriptor.valueに新しい関数を割り当てています。この新しい関数は、メソッドの呼び出し時に引数と戻り値をログ出力する機能を追加しています。元のメソッドはapplyを使用して呼び出され、ログを出力した後にその結果を返します。

デコレーターの適用

次に、上記で作成したデコレーターをクラスのメソッドに適用する方法を示します。

class MyClass {
  @logMethod
  add(a: number, b: number): number {
    return a + b;
  }
}

const obj = new MyClass();
obj.add(2, 3); // ログが出力される

この例では、addメソッドに対して@logMethodデコレーターが適用されています。このメソッドが呼び出されると、引数と戻り値がログに記録されるようになります。

まとめ

メソッドデコレーターを使うことで、クラスメソッドに簡単にログ機能を追加できます。ログ処理をメソッド内に直接書く必要がなく、コードの可読性や保守性を向上させることができます。次は、デコレーター内でログをどのように記録するか、さらに詳しく見ていきます。

デコレーター内でのログ処理の実装

メソッドデコレーターを使用してクラスメソッドにログ機能を追加する際、ログの出力方法や記録内容をカスタマイズすることが可能です。ここでは、デコレーター内でのログ処理の実装をさらに深掘りし、柔軟なログ管理を行う方法を解説します。

ログ出力の具体例

デコレーター内で、メソッドが呼び出された際にログを出力するには、console.logを利用するのが最も簡単です。しかし、実際のアプリケーションでは、ログを外部のログシステムに送信したり、ファイルに保存したりする場合があります。以下は、簡単なconsole.logを使用したログ出力の実装例です。

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const methodName = propertyKey;
    const argsString = JSON.stringify(args);
    const timestamp = new Date().toISOString();

    console.log(`[${timestamp}] Method: ${methodName}, Arguments: ${argsString}`);

    const result = originalMethod.apply(this, args);

    const resultString = JSON.stringify(result);
    console.log(`[${timestamp}] Method: ${methodName}, Returned: ${resultString}`);

    return result;
  };

  return descriptor;
}

実装のポイント

  • タイムスタンプの追加: ログにタイムスタンプを含めることで、メソッドの呼び出し時間や処理の順番を追跡できます。これにより、時間に基づいたメソッドの挙動を把握するのが容易になります。
  • 引数と戻り値のログ出力: メソッドに渡された引数と、実行後の戻り値をJSON形式でログに記録しています。これにより、実行された処理内容を正確に把握できます。

外部ライブラリを用いたログ出力

console.log以外に、実際のプロジェクトではより強力なログ管理ツールを使用することが一般的です。例えば、人気のあるwinstonlog4jsといったNode.js向けのログライブラリを利用することで、ログのフィルタリングや保存先の指定など、より高度なログ管理が可能です。

以下は、winstonを使ったログ出力の例です。

import * as winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const methodName = propertyKey;
    const argsString = JSON.stringify(args);
    const timestamp = new Date().toISOString();

    logger.info(`[${timestamp}] Method: ${methodName}, Arguments: ${argsString}`);

    const result = originalMethod.apply(this, args);

    const resultString = JSON.stringify(result);
    logger.info(`[${timestamp}] Method: ${methodName}, Returned: ${resultString}`);

    return result;
  };

  return descriptor;
}

このコードでは、winstonライブラリを使ってログをコンソールに出力するだけでなく、ファイルにも保存しています。これにより、ログを蓄積して後から確認することができ、運用中のアプリケーションのトラブルシューティングが容易になります。

ログの出力先のカスタマイズ

プロジェクトによっては、ログをファイルに保存するだけでなく、リモートサーバーに送信する必要がある場合もあります。このような場合、ログの出力先をカスタマイズすることが重要です。例えば、APIにログを送信する場合、デコレーター内でHTTPリクエストを行うことも可能です。

async function sendLogToServer(log: string) {
  await fetch('https://example.com/logs', {
    method: 'POST',
    body: JSON.stringify({ log }),
    headers: { 'Content-Type': 'application/json' },
  });
}

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const methodName = propertyKey;
    const argsString = JSON.stringify(args);
    const timestamp = new Date().toISOString();

    const logMessage = `[${timestamp}] Method: ${methodName}, Arguments: ${argsString}`;
    console.log(logMessage);
    await sendLogToServer(logMessage);

    const result = originalMethod.apply(this, args);

    const resultString = JSON.stringify(result);
    const resultLogMessage = `[${timestamp}] Method: ${methodName}, Returned: ${resultString}`;
    console.log(resultLogMessage);
    await sendLogToServer(resultLogMessage);

    return result;
  };

  return descriptor;
}

このように、デコレーター内で柔軟にログ出力処理をカスタマイズできます。

まとめ

デコレーター内でログ処理を実装することで、コードのメンテナンス性が向上し、重要な情報を記録しやすくなります。ログの出力先や内容を自由にカスタマイズすることで、開発環境や本番環境で役立つログシステムを構築することが可能です。次は、ログのフォーマットやフィルタリングの方法について詳しく説明します。

ログのフォーマットとフィルタリングのカスタマイズ

デコレーターを使用してクラスメソッドにログ機能を追加した後、ログのフォーマットやフィルタリングを適切にカスタマイズすることで、効率的なログ管理が可能になります。ここでは、ログのフォーマットをカスタマイズし、必要に応じてフィルタリングする方法について詳しく解説します。

ログのフォーマットをカスタマイズする方法

ログを出力する際、単純にメソッド名や引数を出力するだけでは、後で分析やデバッグが難しくなることがあります。以下では、ログに追加の情報を組み込み、フォーマットをわかりやすくする方法を紹介します。

function logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const methodName = propertyKey;
    const argsString = JSON.stringify(args);
    const timestamp = new Date().toISOString();
    const className = target.constructor.name;

    const logMessage = `[${timestamp}] [Class: ${className}] [Method: ${methodName}] Arguments: ${argsString}`;
    console.log(logMessage);

    const result = originalMethod.apply(this, args);

    const resultString = JSON.stringify(result);
    console.log(`[${timestamp}] [Class: ${className}] [Method: ${methodName}] Returned: ${resultString}`);

    return result;
  };

  return descriptor;
}

ポイント解説

  • タイムスタンプ: ログに呼び出し時間を含めることで、メソッドがいつ実行されたかを把握できるようにしています。
  • クラス名の表示: メソッドがどのクラスのものであるかを明確にするため、target.constructor.nameを使用してクラス名を取得しています。これにより、複数のクラスで同名のメソッドが存在する場合でも、どのクラスのメソッドが実行されたのかがわかります。
  • JSON形式での引数と戻り値の出力: 引数や戻り値をJSON形式でログに出力することで、複数の引数や複雑なデータ構造も正確に記録することができます。

このように、ログフォーマットを柔軟にカスタマイズすることで、ログの可読性を高めることができます。

ログのフィルタリング

ログ出力のフィルタリングを行うことで、必要な情報のみを効率的に記録し、不要な情報をログから除外することができます。例えば、デバッグ時には詳細なログを出力し、運用時にはエラーログのみを記録する、といった運用が可能です。

ログレベルの導入

ログレベルを設定することで、ログの出力内容を制御できます。例えば、infowarnerrorなどのレベルを設定し、必要に応じて出力する内容を変えることができます。以下は、簡単なログレベルを実装した例です。

enum LogLevel {
  INFO,
  WARN,
  ERROR
}

let currentLogLevel: LogLevel = LogLevel.INFO;

function logMethodWithLevel(level: LogLevel) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      if (level >= currentLogLevel) {
        const methodName = propertyKey;
        const argsString = JSON.stringify(args);
        const timestamp = new Date().toISOString();
        const className = target.constructor.name;

        console.log(`[${timestamp}] [${LogLevel[level]}] [Class: ${className}] [Method: ${methodName}] Arguments: ${argsString}`);
      }

      const result = originalMethod.apply(this, args);

      if (level >= currentLogLevel) {
        const resultString = JSON.stringify(result);
        console.log(`[${timestamp}] [${LogLevel[level]}] [Class: ${className}] [Method: ${methodName}] Returned: ${resultString}`);
      }

      return result;
    };

    return descriptor;
  };
}

使い方

class MyClass {
  @logMethodWithLevel(LogLevel.INFO)
  add(a: number, b: number): number {
    return a + b;
  }

  @logMethodWithLevel(LogLevel.ERROR)
  divide(a: number, b: number): number {
    if (b === 0) {
      throw new Error("Division by zero");
    }
    return a / b;
  }
}

const obj = new MyClass();
obj.add(5, 3);  // INFOログが出力される
obj.divide(10, 0);  // ERRORログが出力される

この例では、currentLogLevelに応じてログの出力を制御しています。currentLogLevelLogLevel.ERRORに設定すると、ERRORレベルのログのみが出力され、INFOレベルのログは無視されます。これにより、開発中と運用中で異なるログを出力でき、効率的にフィルタリングを行うことができます。

まとめ

ログのフォーマットとフィルタリングをカスタマイズすることで、必要な情報を整理して出力し、不要なログを削減することが可能になります。フォーマットを統一し、ログレベルを導入することで、システム全体のログ管理が向上します。次は、デコレーターが実行パフォーマンスに与える影響とその最適化方法について見ていきます。

実行パフォーマンスへの影響と最適化のポイント

デコレーターを使用してクラスメソッドにログ機能を追加することで、コードの可読性や保守性が向上しますが、追加の処理が加わるため、実行パフォーマンスに影響を与える可能性があります。特に、ログ出力やフィルタリング、外部リソースとの通信を伴う場合、その影響は無視できないものとなります。ここでは、デコレーターが実行パフォーマンスに与える影響と、最適化のためのポイントを解説します。

デコレーターがパフォーマンスに与える影響

デコレーターを使用する際に注意すべき主なパフォーマンスの要因は、次の通りです。

1. メソッド呼び出し回数の増加

デコレーターは、元のメソッドをラップして追加の処理を行うため、デコレーターが追加されたメソッドを呼び出すたびにログ処理が走ります。頻繁に呼び出されるメソッドにデコレーターを適用すると、その分、処理のオーバーヘッドが増加します。例えば、1秒間に何千回も呼ばれるようなメソッドに対しては、特にパフォーマンスに注意が必要です。

2. ログ出力の負荷

console.logのようなログ出力は、処理が比較的軽量に見えますが、頻繁に呼び出すことで全体のパフォーマンスに悪影響を与えることがあります。また、外部のログシステムにデータを送信する場合、ネットワーク通信による遅延が発生する可能性があります。

3. 非同期処理の追加

デコレーター内で非同期処理を行う場合(例:ログデータをリモートサーバーに送信)、非同期処理が完了するまで待機する必要があるため、メソッドの実行時間が延びる可能性があります。非同期処理を多用する場合は、パフォーマンスの最適化が重要になります。

パフォーマンス最適化のポイント

デコレーターによるパフォーマンス低下を最小限に抑えるためには、いくつかの最適化ポイントがあります。

1. ログの出力条件を制御する

すべてのメソッド呼び出しでログを出力するのではなく、特定の条件下でのみログを出力するように制御します。例えば、開発モードでのみ詳細なログを出力し、本番環境ではエラーログのみを記録するようにします。環境に応じてログレベルを設定することで、不要なログ出力を減らし、パフォーマンスを向上させます。

if (process.env.NODE_ENV === 'development') {
  console.log(`Method ${propertyKey} called with arguments: ${JSON.stringify(args)}`);
}

2. 非同期処理の適切な使用

外部リソースへのログ送信などの重い処理は、非同期で行い、メインのメソッド処理には影響を与えないようにすることが重要です。ただし、非同期処理は可能な限りメインの処理とは独立させるべきです。例えば、メインの処理が完了してからログ送信を行うように設計します。

descriptor.value = function (...args: any[]) {
  const result = originalMethod.apply(this, args);

  // メイン処理終了後に非同期でログ送信
  setTimeout(() => {
    sendLogToServer(`[Method: ${propertyKey}] Arguments: ${JSON.stringify(args)}`);
  }, 0);

  return result;
};

3. ログのバッチ処理

大量のログデータをリアルタイムに送信するのではなく、一定の間隔でログをバッチ処理としてまとめて送信することで、パフォーマンスの向上が期待できます。これにより、頻繁な通信によるオーバーヘッドを削減できます。

let logQueue: string[] = [];

function enqueueLog(log: string) {
  logQueue.push(log);
  if (logQueue.length >= 10) {
    flushLogs();
  }
}

function flushLogs() {
  // バッチ処理でまとめてログ送信
  const logs = logQueue.join('\n');
  sendLogToServer(logs);
  logQueue = [];
}

4. キャッシュやメモ化の活用

頻繁に実行されるメソッドの場合、結果をキャッシュすることで、同じ処理を何度も繰り返さないようにすることができます。これにより、パフォーマンスを大幅に改善できます。メソッドの結果をキャッシュするデコレーターを作成することも可能です。

function memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const cache = new Map();
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };

  return descriptor;
}

まとめ

デコレーターを使ったメソッドのログ機能は便利ですが、適切な最適化を行わないとパフォーマンスに悪影響を与えることがあります。ログの出力を制御し、非同期処理を適切に使用すること、また、バッチ処理やキャッシュを活用することで、パフォーマンスを最適化できます。次は、エラーログの実装方法について解説します。

デコレーターを使ったエラーログの実装

アプリケーション開発において、エラーログの管理は非常に重要です。エラーログを記録することで、異常が発生した箇所や原因を特定し、迅速に対応することができます。デコレーターを使用することで、クラスメソッドで発生した例外を自動的にキャッチし、ログに記録する処理を簡単に追加できます。ここでは、エラーログの実装方法について詳しく解説します。

エラーログ用デコレーターの作成

エラーハンドリングを行うデコレーターを作成し、メソッドで発生した例外をキャッチしてログに記録します。以下のコードは、エラーログを記録するデコレーターの例です。

function logError(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    try {
      // オリジナルメソッドを実行
      return await originalMethod.apply(this, args);
    } catch (error) {
      // エラーログを出力
      const timestamp = new Date().toISOString();
      const className = target.constructor.name;
      console.error(`[${timestamp}] [Class: ${className}] [Method: ${propertyKey}] Error: ${error.message}`);

      // エラーログを外部に送信(オプション)
      await sendErrorLogToServer({
        timestamp,
        className,
        method: propertyKey,
        errorMessage: error.message,
        stack: error.stack,
      });

      // エラーを再スローして、通常のエラーハンドリングに任せる
      throw error;
    }
  };

  return descriptor;
}

コードの説明

  • try-catch構文: デコレーター内部でtry-catchを使用して、メソッド内で発生した例外をキャッチします。
  • エラーメッセージのログ出力: 例外が発生した際に、エラーメッセージやスタックトレースを含めた詳細なログを出力します。
  • 外部サーバーへのエラーログ送信: sendErrorLogToServer関数を使用して、エラーログを外部サーバーに送信する処理も含めています。これにより、エラーログの集中管理が可能になります。
  • エラーの再スロー: ログを記録した後、エラーを再スローして通常のエラーハンドリングプロセスに引き渡します。これにより、エラーログの記録と通常のエラー処理が両立します。

エラーログデコレーターの適用

次に、作成したエラーログ用デコレーターをクラスメソッドに適用します。以下は、エラーログを記録する例です。

class MyService {
  @logError
  async divide(a: number, b: number): Promise<number> {
    if (b === 0) {
      throw new Error("Division by zero");
    }
    return a / b;
  }
}

const service = new MyService();
service.divide(10, 0).catch((error) => {
  console.log("Caught error:", error.message);
});

この例では、divideメソッドで0除算が発生した際に、デコレーターによってエラーログが記録されます。エラーログはコンソールに出力され、オプションで外部サーバーにも送信されます。

外部サービスへのエラーログ送信

実際のプロジェクトでは、エラーログをファイルに保存するだけでなく、外部のエラーログ管理サービス(例えば、Sentry、Datadogなど)に送信することが推奨されます。以下は、エラーログを外部サーバーに送信する関数の例です。

async function sendErrorLogToServer(log: {
  timestamp: string;
  className: string;
  method: string;
  errorMessage: string;
  stack?: string;
}) {
  await fetch('https://error-logging-service.com/logs', {
    method: 'POST',
    body: JSON.stringify(log),
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

この関数は、例外発生時にログを外部サーバーにPOSTリクエストとして送信します。これにより、ログがリモートで保存され、後で解析やモニタリングが可能になります。

エラーログのフォーマットと重要情報の記録

エラーログには、次の情報を含めることが推奨されます。

  • タイムスタンプ: エラーが発生した正確な時間を記録します。
  • クラス名とメソッド名: どのクラスのどのメソッドでエラーが発生したかを明確にします。
  • エラーメッセージ: エラーの内容を具体的に記録します。
  • スタックトレース: エラー発生時のコールスタックを含めることで、エラーの発生箇所や原因を詳細に把握できます。

これにより、エラーログを分析する際に、どの部分で問題が発生したのかを簡単に特定できます。

まとめ

デコレーターを使ってクラスメソッドにエラーログを追加することで、例外発生時に自動的にログを記録し、エラーの追跡が容易になります。特に、外部サービスにエラーログを送信することで、分散したアプリケーションでも集中管理されたログによって、エラー解析や運用監視が可能です。次は、実際のプロジェクトでのデコレーター活用例について見ていきます。

実践例:プロジェクトでのデコレーター活用

デコレーターは、クラスやメソッドに対して機能を簡単に追加できるため、実際のプロジェクトでさまざまな用途に応用できます。特に、ログ機能やエラーログ、トランザクション管理、認証、キャッシュなど、共通の処理を一元管理するのに有効です。ここでは、プロジェクトにおけるデコレーターの具体的な活用例をいくつか紹介します。

1. APIリクエストのログ記録

サーバーアプリケーションでは、APIリクエストの記録が重要です。リクエスト内容やレスポンス、処理時間などをログに残すことで、パフォーマンスの監視やトラブルシューティングに役立ちます。以下は、APIエンドポイントに対してログデコレーターを適用する例です。

function logApiRequest(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const startTime = Date.now();
    const result = await originalMethod.apply(this, args);
    const endTime = Date.now();

    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] [API] ${propertyKey} called. Duration: ${endTime - startTime}ms`);

    return result;
  };

  return descriptor;
}

class ApiController {
  @logApiRequest
  async getUserData(userId: string): Promise<User> {
    // ユーザーデータを取得する処理
    return await fetchUserFromDatabase(userId);
  }
}

この例では、APIリクエストにかかった時間を記録し、どのエンドポイントがどの程度の時間で処理されたかを把握できます。これにより、パフォーマンスのボトルネックを発見しやすくなります。

2. トランザクション管理のデコレーター

データベース操作において、トランザクションを使用して一連の操作を安全に実行することは一般的です。デコレーターを使うことで、メソッドの実行前後にトランザクションを自動的に管理できます。

function transaction(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const db = getDatabaseConnection();
    await db.beginTransaction();

    try {
      const result = await originalMethod.apply(this, args);
      await db.commitTransaction();
      return result;
    } catch (error) {
      await db.rollbackTransaction();
      throw error;
    }
  };

  return descriptor;
}

class UserService {
  @transaction
  async updateUserDetails(userId: string, userDetails: UserDetails): Promise<void> {
    // ユーザーデータの更新処理
    await updateUserInDatabase(userId, userDetails);
  }
}

この例では、updateUserDetailsメソッドに対して、トランザクションの開始、コミット、ロールバックの処理をデコレーターで追加しています。これにより、データベース操作の安全性が向上します。

3. 認証のデコレーター

APIやメソッドが実行される前に、ユーザーの認証を行う必要がある場合、デコレーターを使って認証処理を簡単に追加できます。以下は、認証が必要なメソッドに対してデコレーターを適用する例です。

function authenticate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const user = getCurrentUser();
    if (!user || !user.isAuthenticated) {
      throw new Error("Authentication required");
    }

    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class ProtectedService {
  @authenticate
  getSensitiveData() {
    // 認証されたユーザーのみがアクセスできるデータの取得処理
    return "Sensitive data";
  }
}

この例では、getSensitiveDataメソッドが呼び出される前に、ユーザーが認証されているかどうかを確認しています。認証されていない場合、エラーをスローして処理を中断します。

4. キャッシュのデコレーター

頻繁に呼ばれるメソッドに対してキャッシュを導入することで、パフォーマンスを向上させることができます。デコレーターを使ってキャッシュを管理することで、キャッシュ処理の重複を避け、コードを簡潔に保つことができます。

const cache = new Map<string, any>();

function cacheResult(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const cacheKey = `${propertyKey}-${JSON.stringify(args)}`;
    if (cache.has(cacheKey)) {
      return cache.get(cacheKey);
    }

    const result = originalMethod.apply(this, args);
    cache.set(cacheKey, result);

    return result;
  };

  return descriptor;
}

class DataService {
  @cacheResult
  fetchData(query: string): string {
    // データ取得処理(キャッシュ対象)
    return performExpensiveQuery(query);
  }
}

この例では、fetchDataメソッドの結果がキャッシュされており、同じ引数で呼び出された場合はキャッシュから結果が返されます。これにより、重い処理を何度も実行することを避け、パフォーマンスが向上します。

まとめ

デコレーターは、実際のプロジェクトで共通の処理を簡単に管理し、コードのメンテナンス性を向上させる強力なツールです。APIログの記録、トランザクション管理、認証、キャッシュなど、さまざまな場面で活用することができ、アプリケーション全体の効率化と信頼性を高めることが可能です。次は、デコレーターのテストとデバッグ方法について説明します。

デコレーターのテストとデバッグ方法

デコレーターを使用したコードが期待どおりに動作しているか確認するためには、テストとデバッグが不可欠です。特にデコレーターは、コードの一部を動的に変更するため、通常のメソッドとは異なる動作をする場合があります。ここでは、デコレーターをテストするための方法と、デバッグのポイントを詳しく説明します。

1. ユニットテストでデコレーターを検証する

デコレーターの動作を確認するために、ユニットテストを行うのは非常に効果的です。以下は、デコレーターを適用したメソッドをテストするための基本的な方法を示します。例えば、Jestなどのテストフレームワークを使って、メソッドデコレーターが正しく動作しているかを検証します。

// テスト対象のデコレーター
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method ${propertyKey} called with args: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

// テスト対象のクラス
class TestClass {
  @logMethod
  sum(a: number, b: number): number {
    return a + b;
  }
}

// Jestを使ったテスト
describe('logMethod Decorator', () => {
  let testClass: TestClass;

  beforeEach(() => {
    testClass = new TestClass();
    console.log = jest.fn(); // console.logをモック化
  });

  test('should log method name and arguments', () => {
    testClass.sum(2, 3);
    expect(console.log).toHaveBeenCalledWith('Method sum called with args: [2,3]');
  });

  test('should return correct sum', () => {
    const result = testClass.sum(2, 3);
    expect(result).toBe(5);
  });
});

ポイント解説

  • モック化: console.logをモック化することで、ログ出力の内容が正しく行われているかを確認できます。このように、デコレーターの副作用であるログ出力や外部呼び出しをテストする際に、モックやスタブを使用するのが有効です。
  • ユニットテストの粒度: デコレーターを適用したメソッドに対して、メソッド自体の動作(ここではsumメソッド)と、デコレーターによるログ出力の動作を個別にテストしています。

2. テスト対象のメソッドとデコレーターの相互作用を確認する

デコレーターのテストでは、メソッド本来の機能とデコレーターによる追加機能の相互作用を検証する必要があります。特に、メソッドの戻り値が期待通りかつデコレーターが正しく動作しているかを確認します。

例えば、デコレーターがエラーハンドリングを追加する場合、エラーの発生時に正しくキャッチされているかをテストします。

// エラーハンドリングデコレーター
function catchError(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    try {
      return originalMethod.apply(this, args);
    } catch (error) {
      console.error(`Error in ${propertyKey}: ${error.message}`);
      return null; // エラーハンドリング後、nullを返す
    }
  };

  return descriptor;
}

// クラスメソッド
class MyClass {
  @catchError
  divide(a: number, b: number): number {
    if (b === 0) throw new Error("Division by zero");
    return a / b;
  }
}

// テスト
describe('catchError Decorator', () => {
  let myClass: MyClass;

  beforeEach(() => {
    myClass = new MyClass();
    console.error = jest.fn(); // console.errorをモック化
  });

  test('should handle division by zero', () => {
    const result = myClass.divide(10, 0);
    expect(result).toBeNull(); // エラーハンドリング後はnullが返される
    expect(console.error).toHaveBeenCalledWith('Error in divide: Division by zero');
  });
});

このように、メソッド内で例外が発生した場合に、デコレーターが正しくエラーハンドリングし、期待通りの動作が行われているかをテストします。

3. デバッグ方法

デコレーターのデバッグは、通常のメソッドとは異なる点があります。特に、デコレーターがメソッドの実行前後に追加処理を行うため、デバッグ時に追跡する場所が増えます。以下にデバッグのポイントを示します。

デコレーター内部のログ出力

デコレーター内での処理フローを追うために、ログ出力を活用します。デコレーター内に適切なconsole.logを追加することで、どのタイミングでメソッドが実行され、デコレーターの処理がどのように影響しているかを確認できます。

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Entering method ${propertyKey} with arguments: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`Exiting method ${propertyKey} with result: ${JSON.stringify(result)}`);
    return result;
  };

  return descriptor;
}

このように、メソッドの実行前後でログを出力することで、デコレーターによってメソッドがどのように変更されているかをデバッグしやすくなります。

ブレークポイントの設定

デコレーター内の関数にブレークポイントを設定して、デバッグツールを使って処理の流れを確認します。特に、descriptor.valueをラップしている部分にブレークポイントを設定することで、メソッドの呼び出しタイミングや引数、戻り値を詳細に確認できます。

まとめ

デコレーターのテストとデバッグは、メソッド本体とデコレーターによる追加機能が正しく連携しているかを確認するために重要です。モックやスタブを使って副作用を検証し、デコレーター内部のログやブレークポイントを活用してデバッグを行うことで、問題の早期発見が可能になります。次は、クラス全体に適用するデコレーターの応用例について解説します。

クラス全体に適用するデコレーターの応用例

TypeScriptでは、メソッドだけでなく、クラス全体にデコレーターを適用することも可能です。クラスデコレーターを使用することで、すべてのメソッドに共通するロジックを一元管理したり、クラス自体の振る舞いを変更することができます。ここでは、クラス全体にデコレーターを適用する応用例について解説します。

1. クラスの初期化時に共通処理を追加する

クラスデコレーターを使用すると、クラスのインスタンス化時に共通の初期化処理を追加することができます。例えば、クラスのインスタンスが生成されるたびにログを出力するデコレーターを作成してみましょう。

function logClass(target: any) {
  // コンストラクタをラップして、インスタンス生成時にログを出力
  const originalConstructor = target;

  function newConstructor(...args: any[]) {
    console.log(`Creating instance of ${originalConstructor.name} with args: ${JSON.stringify(args)}`);
    return new originalConstructor(...args);
  }

  // 新しいコンストラクタを返す
  return newConstructor;
}

@logClass
class MyClass {
  constructor(private name: string, private age: number) {}

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const instance = new MyClass('John', 30); // インスタンス生成時にログが出力される
instance.greet();

この例では、@logClassデコレーターがクラスMyClassに適用されています。クラスのインスタンスが生成されるたびに、コンストラクタ引数をログに記録する処理が追加されています。

2. 全メソッドに共通処理を適用する

クラスデコレーターを使って、クラス内のすべてのメソッドに共通の処理を適用することができます。以下は、すべてのメソッドに対して自動的にログを記録するデコレーターの例です。

function logAllMethods(target: any) {
  // クラスのプロトタイプを取得
  const classPrototype = target.prototype;

  // クラスのメソッドに対してループを実行
  Object.getOwnPropertyNames(classPrototype).forEach((methodName) => {
    const originalMethod = classPrototype[methodName];

    // メソッドのみが対象
    if (typeof originalMethod === 'function' && methodName !== 'constructor') {
      classPrototype[methodName] = function (...args: any[]) {
        console.log(`Method ${methodName} called with args: ${JSON.stringify(args)}`);
        return originalMethod.apply(this, args);
      };
    }
  });
}

@logAllMethods
class UserService {
  getUser(userId: string) {
    return `User data for ${userId}`;
  }

  saveUser(userId: string, userData: any) {
    console.log(`Saving data for ${userId}`);
  }
}

const service = new UserService();
service.getUser('123'); // メソッド呼び出し時にログが出力される
service.saveUser('123', { name: 'Alice' });

この例では、@logAllMethodsデコレーターがクラスUserServiceに適用されています。クラス内のすべてのメソッドに対して、呼び出し時のログを出力する処理が自動的に追加されています。これにより、すべてのメソッドの動作を簡単にトレースできるようになります。

3. クラスのプロパティに共通処理を適用する

クラスデコレーターを使用して、クラスのプロパティに対しても共通処理を適用することができます。以下は、クラスのプロパティが設定された際に自動的に検証処理を行うデコレーターの例です。

function validateProperties(target: any) {
  const originalConstructor = target;

  function newConstructor(...args: any[]) {
    const instance = new originalConstructor(...args);

    // インスタンスのすべてのプロパティを検証
    Object.keys(instance).forEach((key) => {
      if (instance[key] == null || instance[key] === '') {
        throw new Error(`Property ${key} is invalid`);
      }
    });

    return instance;
  }

  return newConstructor;
}

@validateProperties
class Product {
  constructor(public name: string, public price: number) {}
}

try {
  const product = new Product('', 100); // エラー: Property name is invalid
} catch (error) {
  console.log(error.message);
}

この例では、@validatePropertiesデコレーターがクラスProductに適用され、インスタンス生成時にプロパティの値が検証されます。空文字やnullのプロパティがある場合、エラーがスローされます。

4. クラスのメタデータを追加する

クラスデコレーターを使って、クラスにメタデータを付与し、後からそのメタデータを利用することができます。例えば、認証やアクセス権限の管理をクラスごとに設定することが可能です。

function requiresAuth(role: string) {
  return function (target: any) {
    Reflect.defineMetadata('role', role, target);
  };
}

@requiresAuth('admin')
class AdminService {
  deleteUser(userId: string) {
    console.log(`User ${userId} deleted`);
  }
}

// メタデータを取得する
const role = Reflect.getMetadata('role', AdminService);
console.log(`Required role: ${role}`); // Required role: admin

この例では、@requiresAuthデコレーターを使って、クラスにアクセス権限のメタデータを付与しています。メタデータは、クラスに対する特定の操作や条件を後から確認するために使用できます。

まとめ

クラス全体にデコレーターを適用することで、クラスのインスタンス生成時の処理や、すべてのメソッドに共通の機能を簡単に追加できます。これにより、コードの重複を避け、クリーンなアーキテクチャを実現できます。また、クラスのプロパティ検証やメタデータの追加もデコレーターを活用することで柔軟に実装できます。次は、これまでの内容をまとめます。

まとめ

本記事では、TypeScriptにおけるデコレーターを使ったクラスメソッドへのログ機能の追加方法について詳しく解説しました。デコレーターを活用することで、メソッド単位やクラス全体に対して共通の処理を簡単に適用でき、ログ記録やエラーハンドリング、パフォーマンスの最適化など、さまざまなシーンで役立つツールとなります。また、実践的なプロジェクト例を通して、トランザクション管理や認証、キャッシュ処理の自動化など、デコレーターの幅広い応用を確認しました。

適切にデコレーターを使うことで、コードの保守性を高めつつ、効率的な開発が可能となります。

コメント

コメントする

目次