JavaScriptで関数デコレーターを作成する方法を徹底解説

JavaScriptにおけるデコレーターの基本概念とその用途について解説します。デコレーターは、関数やメソッドに追加の機能や振る舞いを付加するための設計パターンです。PythonやJavaなど他のプログラミング言語でも広く使われていますが、JavaScriptでも同様の手法を用いることができます。デコレーターを使用することで、コードの再利用性を高め、関数の振る舞いを動的に変更することが可能になります。本記事では、JavaScriptにおけるデコレーターの基本から応用例まで、詳細に解説していきます。これにより、デコレーターの効果的な活用方法を理解し、実際のプロジェクトでの活用に役立てていただけることを目指しています。

目次

デコレーターの基本概念

デコレーターとは、既存の関数やメソッドに新しい機能や振る舞いを追加するためのデザインパターンです。デコレーターを使用することで、コードの再利用性と可読性を向上させることができます。

デコレーターの働き

デコレーターは、ある関数を他の関数でラップすることで機能します。このラップされた関数は、元の関数の前後に追加の処理を行ったり、元の関数の出力を変更したりすることができます。これにより、元の関数の動作を変更せずに新しい機能を付加することが可能です。

デコレーターの利点

デコレーターを使用することで、以下のような利点があります。

コードの再利用性

共通の機能をデコレーターとして切り出すことで、同じコードを繰り返し記述する必要がなくなります。

コードの可読性

デコレーターを使用することで、関数の主なロジックと補助的な機能を明確に分離できます。

機能の追加と変更が容易

デコレーターを使うことで、新しい機能を既存のコードに追加する際に、元のコードを変更せずに済みます。

デコレーターの例

例えば、関数の実行時間を計測するデコレーターを考えてみましょう。このデコレーターは、関数の前後にタイマーを設定し、関数の実行時間をログに出力します。これにより、特定の関数のパフォーマンスを簡単に測定できます。

このように、デコレーターはコードの再利用性を高め、特定の機能を簡単に追加できる強力なツールです。次のセクションでは、JavaScriptでのデコレーターの構文と構造について詳しく見ていきます。

デコレーターの構文と構造

JavaScriptでデコレーターを作成するための基本構文と構造について説明します。デコレーターは、関数を引数に取り、新しい関数を返す高階関数として実装されます。

基本構文

JavaScriptにおけるデコレーターは、次のような構文で作成されます。

function decorator(func) {
    return function(...args) {
        // 追加の処理(前処理)
        const result = func(...args);
        // 追加の処理(後処理)
        return result;
    };
}

この例では、decorator関数が他の関数funcを引数に取り、ラップされた新しい関数を返します。新しい関数の内部で、元の関数funcが呼び出され、その前後に追加の処理が行われます。

シンプルなデコレーターの例

関数の実行時間を計測するデコレーターの例を見てみましょう。

function timingDecorator(func) {
    return function(...args) {
        console.log('Starting timer...');
        const start = performance.now();

        const result = func(...args);

        const end = performance.now();
        console.log(`Function executed in ${end - start} milliseconds`);

        return result;
    };
}

このtimingDecoratorは、関数の実行前にタイマーを開始し、実行後にその経過時間をログに出力します。

デコレーターの適用

デコレーターを関数に適用するには、次のようにします。

function exampleFunction(x, y) {
    return x + y;
}

const decoratedFunction = timingDecorator(exampleFunction);

console.log(decoratedFunction(5, 3)); // 実行時間を計測して出力

exampleFunctionがデコレーターによってラップされ、実行時間が計測されるようになります。

ES6クラスでのデコレーター

JavaScriptのクラスメソッドにデコレーターを適用することもできます。ES6以降では、クラスメソッドにデコレーターを直接適用する構文が提案されていますが、標準化はされていません。以下は、その例です。

class Example {
    @timingDecorator
    method(x, y) {
        return x + y;
    }
}

このように、デコレーターを使用してクラスメソッドに追加機能を簡単に付加することができます。

次のセクションでは、基本的な関数デコレーターの作成方法についてさらに詳しく説明します。

関数デコレーターの基本的な作成方法

関数デコレーターを作成する際の基本的な手順について説明します。ここでは、具体的な例を通して、デコレーターをどのように作成し、使用するかを理解していきます。

シンプルな関数デコレーターの作成

まずは、シンプルな関数デコレーターを作成してみましょう。このデコレーターは、関数の呼び出しをログに記録する機能を追加します。

function logDecorator(func) {
    return function(...args) {
        console.log(`Calling function ${func.name} with arguments:`, args);
        const result = func(...args);
        console.log(`Function ${func.name} returned:`, result);
        return result;
    };
}

このlogDecoratorは、関数の呼び出し前に引数を、呼び出し後に戻り値をログに記録します。

デコレーターの適用と動作確認

次に、このデコレーターを具体的な関数に適用してみます。

function add(a, b) {
    return a + b;
}

const decoratedAdd = logDecorator(add);

console.log(decoratedAdd(2, 3)); // 関数呼び出しがログに記録される

この例では、add関数がlogDecoratorによってラップされ、関数の呼び出しと結果がログに記録されます。

非同期関数へのデコレーターの適用

非同期関数にもデコレーターを適用することができます。非同期関数をラップする場合、デコレーター内部でawaitを使用する必要があります。

function asyncLogDecorator(func) {
    return async function(...args) {
        console.log(`Calling async function ${func.name} with arguments:`, args);
        const result = await func(...args);
        console.log(`Async function ${func.name} returned:`, result);
        return result;
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    return response.json();
}

const decoratedFetchData = asyncLogDecorator(fetchData);

decoratedFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data));

このasyncLogDecoratorは、非同期関数fetchDataをラップし、その呼び出しと結果をログに記録します。

デコレーターを使用する際の注意点

デコレーターを使用する際には、以下の点に注意する必要があります。

  • 関数の元のコンテキスト: デコレーター内でthisを使用する場合、元の関数のコンテキストを保持するように注意が必要です。
  • 引数と戻り値の正確な処理: デコレーターが関数の引数や戻り値を正しく処理するように設計します。
  • エラーハンドリング: デコレーター内で発生する可能性のあるエラーを適切に処理します。

次のセクションでは、引数を持つデコレーターの作成方法について詳しく説明します。

引数を持つデコレーターの作成

デコレーターに引数を渡すことで、より柔軟な機能を持たせることができます。ここでは、引数を持つデコレーターの作成方法について説明します。

引数を持つデコレーターの基本構造

引数を持つデコレーターは、デコレーター関数自体がさらに関数を返す構造になります。このようにすることで、デコレーターに設定を渡すことができます。

function logDecoratorWithArgs(logLevel) {
    return function(func) {
        return function(...args) {
            if (logLevel === 'verbose') {
                console.log(`Calling function ${func.name} with arguments:`, args);
            }
            const result = func(...args);
            if (logLevel === 'verbose') {
                console.log(`Function ${func.name} returned:`, result);
            }
            return result;
        };
    };
}

この例では、logDecoratorWithArgsがログレベルを引数として受け取り、その後にデコレーター関数を返します。このデコレーター関数は、指定されたログレベルに応じてログの出力を行います。

デコレーターの適用と動作確認

次に、このデコレーターを具体的な関数に適用してみます。

function multiply(a, b) {
    return a * b;
}

const verboseMultiply = logDecoratorWithArgs('verbose')(multiply);
const silentMultiply = logDecoratorWithArgs('silent')(multiply);

console.log(verboseMultiply(2, 3)); // 詳細なログが出力される
console.log(silentMultiply(2, 3));  // ログは出力されない

この例では、multiply関数がログレベルに応じてラップされ、verboseログレベルの場合にのみログが出力されます。

実用的な引数付きデコレーターの例

次に、実用的な例として、関数の実行時間を特定の閾値を超えた場合に警告を表示するデコレーターを作成します。

function timingDecorator(threshold) {
    return function(func) {
        return function(...args) {
            const start = performance.now();
            const result = func(...args);
            const end = performance.now();
            const duration = end - start;

            if (duration > threshold) {
                console.warn(`Function ${func.name} took ${duration} milliseconds, exceeding the threshold of ${threshold} milliseconds.`);
            }

            return result;
        };
    };
}

function slowFunction() {
    // Simulate a slow function
    for (let i = 0; i < 1e7; i++) {}
    return 'Done';
}

const monitoredSlowFunction = timingDecorator(100)(slowFunction);

console.log(monitoredSlowFunction()); // 実行時間が閾値を超えた場合に警告が表示される

このtimingDecoratorは、指定された閾値を超える実行時間を持つ関数呼び出しに対して警告を表示します。

デコレーターを使う際の柔軟性

引数を持つデコレーターを使用することで、様々な条件や設定に応じて関数の振る舞いを柔軟に変更できます。これにより、コードの再利用性がさらに向上し、特定の要件に応じたカスタマイズが容易になります。

次のセクションでは、デコレーターの実用例についてさらに詳しく見ていきます。

デコレーターの実用例

デコレーターは、さまざまな実用的なシナリオで活用できます。ここでは、日常の開発において役立つ具体的なデコレーターの使用例をいくつか紹介します。

入力の検証を行うデコレーター

関数の引数を検証するデコレーターは、データの正当性を保証するために役立ちます。以下の例では、数値引数が正の数であることを確認するデコレーターを作成します。

function positiveNumberDecorator(func) {
    return function(...args) {
        if (args.some(arg => typeof arg !== 'number' || arg <= 0)) {
            throw new Error('All arguments must be positive numbers.');
        }
        return func(...args);
    };
}

function addPositiveNumbers(a, b) {
    return a + b;
}

const safeAdd = positiveNumberDecorator(addPositiveNumbers);

console.log(safeAdd(5, 3)); // 8
console.log(safeAdd(-5, 3)); // エラーがスローされる

このデコレーターは、引数が正の数であることをチェックし、条件を満たさない場合はエラーをスローします。

リトライ機能を追加するデコレーター

リトライ機能を追加するデコレーターは、失敗した操作を再試行するために使用されます。以下の例では、指定された回数だけ再試行するデコレーターを作成します。

function retryDecorator(retries) {
    return function(func) {
        return async function(...args) {
            for (let attempt = 0; attempt < retries; attempt++) {
                try {
                    return await func(...args);
                } catch (error) {
                    if (attempt === retries - 1) {
                        throw error;
                    }
                }
            }
        };
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error('Network response was not ok.');
    }
    return response.json();
}

const fetchDataWithRetry = retryDecorator(3)(fetchData);

fetchDataWithRetry('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Fetch failed after 3 retries:', error));

このretryDecoratorは、指定された回数だけ関数を再試行し、すべての試行が失敗した場合にエラーをスローします。

アクセス制御を行うデコレーター

アクセス制御を行うデコレーターは、特定の条件下でのみ関数を実行するように制御できます。以下の例では、ユーザーのロールに基づいて関数の実行を制御するデコレーターを作成します。

function roleDecorator(requiredRole) {
    return function(func) {
        return function(...args) {
            const userRole = getUserRole(); // ユーザーのロールを取得する関数
            if (userRole !== requiredRole) {
                throw new Error(`Insufficient permissions. Required role: ${requiredRole}`);
            }
            return func(...args);
        };
    };
}

function getUserRole() {
    // 実際のアプリケーションでは、ここでユーザーのロールを取得する処理を行う
    return 'user';
}

function deleteResource(resourceId) {
    console.log(`Resource ${resourceId} deleted.`);
}

const deleteResourceAsAdmin = roleDecorator('admin')(deleteResource);

try {
    deleteResourceAsAdmin(123); // ユーザーのロールが 'admin' でない場合、エラーがスローされる
} catch (error) {
    console.error(error.message);
}

このデコレーターは、関数の実行前にユーザーのロールをチェックし、必要なロールを持っていない場合はエラーをスローします。

これらの例からわかるように、デコレーターを使用することで、コードの再利用性を高め、様々な機能を簡単に追加することができます。次のセクションでは、デコレーターを使ったロギング機能の追加方法について説明します。

デコレーターを使ったロギング機能の追加

ロギングは、デバッグやパフォーマンスの監視において重要な役割を果たします。デコレーターを使って関数にロギング機能を追加することで、コードをより簡潔で保守しやすくすることができます。

基本的なロギングデコレーターの作成

まず、関数の実行前後にログを記録する基本的なロギングデコレーターを作成します。

function loggingDecorator(func) {
    return function(...args) {
        console.log(`Calling function ${func.name} with arguments:`, args);
        const result = func(...args);
        console.log(`Function ${func.name} returned:`, result);
        return result;
    };
}

function sum(a, b) {
    return a + b;
}

const loggedSum = loggingDecorator(sum);

console.log(loggedSum(2, 3)); // 関数の呼び出しと結果がログに記録される

このloggingDecoratorは、関数の呼び出し時に引数と戻り値をログに記録します。

非同期関数のロギングデコレーター

非同期関数に対しても同様にロギングデコレーターを適用できます。非同期関数の戻り値はPromiseであるため、awaitを使用して結果を取得します。

function asyncLoggingDecorator(func) {
    return async function(...args) {
        console.log(`Calling async function ${func.name} with arguments:`, args);
        const result = await func(...args);
        console.log(`Async function ${func.name} returned:`, result);
        return result;
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    return response.json();
}

const loggedFetchData = asyncLoggingDecorator(fetchData);

loggedFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

このasyncLoggingDecoratorは、非同期関数の呼び出し時に引数と戻り値をログに記録します。

詳細なロギングデコレーターの作成

詳細な情報を記録するために、ロギングデコレーターを拡張します。例えば、関数の実行時間も記録するデコレーターを作成します。

function detailedLoggingDecorator(func) {
    return function(...args) {
        console.log(`Calling function ${func.name} with arguments:`, args);
        const start = performance.now();
        const result = func(...args);
        const end = performance.now();
        console.log(`Function ${func.name} returned:`, result);
        console.log(`Execution time: ${end - start} milliseconds`);
        return result;
    };
}

function multiply(a, b) {
    return a * b;
}

const loggedMultiply = detailedLoggingDecorator(multiply);

console.log(loggedMultiply(4, 5)); // 関数の呼び出し、結果、実行時間がログに記録される

このdetailedLoggingDecoratorは、関数の実行時間もログに記録します。

デコレーターの適用範囲を広げる

ロギングデコレーターをクラスメソッドに適用することもできます。クラス内のメソッドに対してデコレーターを使用することで、特定のメソッドにロギング機能を簡単に追加できます。

class Calculator {
    @loggingDecorator
    add(a, b) {
        return a + b;
    }

    @detailedLoggingDecorator
    multiply(a, b) {
        return a * b;
    }
}

const calculator = new Calculator();
console.log(calculator.add(10, 20)); // ロギング機能が適用されたメソッド
console.log(calculator.multiply(10, 20)); // 詳細なロギング機能が適用されたメソッド

この例では、Calculatorクラスのメソッドaddmultiplyに対して異なるロギングデコレーターを適用しています。

デコレーターを使用することで、関数やメソッドにロギング機能を簡単に追加でき、コードの可読性と保守性を向上させることができます。次のセクションでは、デコレーターを使ったキャッシュ機能の実装方法について説明します。

デコレーターを使ったキャッシュ機能の実装

キャッシュ機能を追加することで、特定の関数の計算結果を保存し、同じ入力に対する再計算を避けることができます。これにより、計算の効率が向上し、パフォーマンスが改善されます。ここでは、デコレーターを使ってキャッシュ機能を実装する方法を紹介します。

基本的なキャッシュデコレーターの作成

まず、シンプルなキャッシュデコレーターを作成してみましょう。このデコレーターは、関数の引数と結果をマップに保存します。

function cacheDecorator(func) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log('Fetching from cache:', key);
            return cache.get(key);
        }
        const result = func(...args);
        cache.set(key, result);
        return result;
    };
}

function slowFunction(num) {
    // 重い計算をシミュレート
    for (let i = 0; i < 1e6; i++) {}
    return num * num;
}

const cachedSlowFunction = cacheDecorator(slowFunction);

console.log(cachedSlowFunction(5)); // 計算を実行
console.log(cachedSlowFunction(5)); // キャッシュから取得

このcacheDecoratorは、関数の引数を文字列化してキャッシュのキーとして使用し、結果をキャッシュに保存します。同じ引数で関数が呼び出された場合、キャッシュから結果を取得します。

非同期関数のキャッシュデコレーター

非同期関数にもキャッシュデコレーターを適用できます。非同期関数の結果はPromiseであるため、キャッシュにはPromiseを保存します。

function asyncCacheDecorator(func) {
    const cache = new Map();
    return async function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log('Fetching from cache:', key);
            return cache.get(key);
        }
        const result = await func(...args);
        cache.set(key, result);
        return result;
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    return response.json();
}

const cachedFetchData = asyncCacheDecorator(fetchData);

cachedFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

// 同じURLで再度呼び出した場合はキャッシュから取得
cachedFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

このasyncCacheDecoratorは、非同期関数の結果をキャッシュし、同じ引数で呼び出された場合にキャッシュから結果を返します。

キャッシュの有効期限を設定するデコレーター

キャッシュデータの有効期限を設定することで、古いデータの使用を避けることができます。以下の例では、有効期限を設定したキャッシュデコレーターを作成します。

function expiringCacheDecorator(func, ttl) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        const now = Date.now();

        if (cache.has(key)) {
            const { value, expiry } = cache.get(key);
            if (now < expiry) {
                console.log('Fetching from cache:', key);
                return value;
            }
            cache.delete(key); // キャッシュを無効化
        }

        const result = func(...args);
        cache.set(key, { value: result, expiry: now + ttl });
        return result;
    };
}

function heavyComputation(num) {
    // 重い計算をシミュレート
    for (let i = 0; i < 1e6; i++) {}
    return num * num;
}

const cachedComputation = expiringCacheDecorator(heavyComputation, 5000);

console.log(cachedComputation(3)); // 計算を実行
setTimeout(() => console.log(cachedComputation(3)), 2000); // キャッシュから取得
setTimeout(() => console.log(cachedComputation(3)), 6000); // 再計算を実行

このexpiringCacheDecoratorは、キャッシュに保存する際に有効期限を設定し、有効期限が切れたデータはキャッシュから削除されるようにします。

キャッシュデコレーターを使用することで、関数のパフォーマンスを大幅に改善できます。次のセクションでは、エラーハンドリングデコレーターの作成方法について説明します。

エラーハンドリングデコレーターの作成

エラーハンドリングは、堅牢なアプリケーションを構築するために不可欠な要素です。デコレーターを使用して関数のエラーハンドリングを一元化することで、コードの可読性と保守性を向上させることができます。ここでは、エラーハンドリングデコレーターの作成方法を説明します。

基本的なエラーハンドリングデコレーター

まず、基本的なエラーハンドリングデコレーターを作成してみましょう。このデコレーターは、関数内で発生したエラーをキャッチし、ログに記録します。

function errorHandlingDecorator(func) {
    return function(...args) {
        try {
            return func(...args);
        } catch (error) {
            console.error(`Error in function ${func.name}:`, error);
            throw error; // エラーを再スロー
        }
    };
}

function riskyOperation(a, b) {
    if (b === 0) {
        throw new Error('Division by zero');
    }
    return a / b;
}

const safeOperation = errorHandlingDecorator(riskyOperation);

try {
    console.log(safeOperation(10, 2)); // 正常に動作
    console.log(safeOperation(10, 0)); // エラーがキャッチされログに記録される
} catch (error) {
    console.error('Caught an error:', error.message);
}

このerrorHandlingDecoratorは、関数の実行中にエラーが発生した場合にエラーをキャッチし、ログに記録します。

非同期関数のエラーハンドリングデコレーター

非同期関数に対してもエラーハンドリングデコレーターを適用できます。非同期関数のエラーはPromiseを使用して処理します。

function asyncErrorHandlingDecorator(func) {
    return async function(...args) {
        try {
            return await func(...args);
        } catch (error) {
            console.error(`Error in async function ${func.name}:`, error);
            throw error; // エラーを再スロー
        }
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error('Network response was not ok.');
    }
    return response.json();
}

const safeFetchData = asyncErrorHandlingDecorator(fetchData);

safeFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Caught an error:', error.message));

このasyncErrorHandlingDecoratorは、非同期関数内で発生したエラーをキャッチし、ログに記録します。

カスタムエラーハンドリングの実装

特定のエラーに対してカスタム処理を行うデコレーターを作成することもできます。例えば、再試行を行うエラーハンドリングデコレーターを作成します。

function retryErrorHandlingDecorator(func, retries = 3) {
    return async function(...args) {
        for (let attempt = 1; attempt <= retries; attempt++) {
            try {
                return await func(...args);
            } catch (error) {
                console.error(`Error in function ${func.name} on attempt ${attempt}:`, error);
                if (attempt === retries) {
                    throw error; // 最後の試行でエラーを再スロー
                }
            }
        }
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error('Network response was not ok.');
    }
    return response.json();
}

const retryFetchData = retryErrorHandlingDecorator(fetchData, 3);

retryFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Failed after 3 retries:', error.message));

このretryErrorHandlingDecoratorは、指定された回数だけ関数の実行を再試行し、すべての試行が失敗した場合にエラーをスローします。

エラーハンドリングデコレーターの適用例

エラーハンドリングデコレーターは、さまざまなシナリオで役立ちます。例えば、APIリクエスト、ファイル操作、ユーザー入力の検証など、エラーが発生しやすい操作に対して適用できます。

const safeFileOperation = errorHandlingDecorator(fileOperation);
const safeUserInputValidation = errorHandlingDecorator(userInputValidation);
const safeApiRequest = asyncErrorHandlingDecorator(apiRequest);

try {
    safeFileOperation('path/to/file');
} catch (error) {
    console.error('File operation failed:', error.message);
}

safeApiRequest('https://api.example.com/data')
    .then(data => console.log('API request succeeded:', data))
    .catch(error => console.error('API request failed:', error.message));

エラーハンドリングデコレーターを使用することで、コードの保守性と可読性を向上させることができます。次のセクションでは、複数のデコレーターを組み合わせて使用する方法について解説します。

デコレーターの組み合わせと応用

デコレーターを組み合わせて使用することで、関数に対して複数の機能を追加することができます。ここでは、複数のデコレーターを効果的に組み合わせる方法と、その応用例について解説します。

複数のデコレーターを適用する方法

複数のデコレーターを関数に適用する際は、デコレーターを順番に適用していきます。以下の例では、ロギングデコレーターとキャッシュデコレーターを組み合わせて適用します。

function loggingDecorator(func) {
    return function(...args) {
        console.log(`Calling function ${func.name} with arguments:`, args);
        const result = func(...args);
        console.log(`Function ${func.name} returned:`, result);
        return result;
    };
}

function cacheDecorator(func) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache.has(key)) {
            console.log('Fetching from cache:', key);
            return cache.get(key);
        }
        const result = func(...args);
        cache.set(key, result);
        return result;
    };
}

function add(a, b) {
    return a + b;
}

const decoratedAdd = loggingDecorator(cacheDecorator(add));

console.log(decoratedAdd(1, 2)); // 計算結果をキャッシュし、ログを出力
console.log(decoratedAdd(1, 2)); // キャッシュから取得し、ログを出力

この例では、add関数にまずキャッシュデコレーターを適用し、その後にロギングデコレーターを適用しています。

デコレーターの順序に注意

デコレーターを適用する順序は重要です。適用順序によって関数の振る舞いが変わることがあります。例えば、先にキャッシュデコレーターを適用し、その後にロギングデコレーターを適用すると、キャッシュのヒット時にもログが出力されますが、逆にするとキャッシュのヒット時にはログが出力されません。

実用的なデコレーターの組み合わせ例

次に、エラーハンドリング、ロギング、キャッシュを組み合わせた実用的な例を示します。

function errorHandlingDecorator(func) {
    return function(...args) {
        try {
            return func(...args);
        } catch (error) {
            console.error(`Error in function ${func.name}:`, error);
            throw error;
        }
    };
}

const decoratedAddWithAllFeatures = errorHandlingDecorator(loggingDecorator(cacheDecorator(add)));

try {
    console.log(decoratedAddWithAllFeatures(1, 2)); // ログ出力、キャッシュ保存、エラーハンドリング
    console.log(decoratedAddWithAllFeatures(1, 2)); // ログ出力、キャッシュヒット、エラーハンドリング
} catch (error) {
    console.error('Caught an error:', error.message);
}

この例では、add関数に対してキャッシュ、ロギング、エラーハンドリングのデコレーターを順に適用しています。

クラスメソッドへのデコレーター適用

クラスのメソッドにもデコレーターを適用することができます。以下の例では、クラスメソッドに対して複数のデコレーターを適用します。

class Calculator {
    @errorHandlingDecorator
    @loggingDecorator
    @cacheDecorator
    add(a, b) {
        return a + b;
    }
}

const calculator = new Calculator();
try {
    console.log(calculator.add(10, 20)); // キャッシュ、ログ、エラーハンドリングのデコレーションが適用される
    console.log(calculator.add(10, 20)); // キャッシュヒット、ログ、エラーハンドリングのデコレーションが適用される
} catch (error) {
    console.error('Caught an error:', error.message);
}

この例では、クラスCalculatoraddメソッドに対して複数のデコレーターを適用しています。

デコレーターの応用例

デコレーターを応用することで、様々な機能を簡単に追加できます。例えば、アクセス制御、データのフォーマット、リクエストの検証、パフォーマンスの計測など、幅広い用途に対応可能です。

function accessControlDecorator(requiredRole) {
    return function(func) {
        return function(...args) {
            const userRole = getUserRole(); // 実際のアプリケーションではユーザーのロールを取得する処理
            if (userRole !== requiredRole) {
                throw new Error(`Insufficient permissions. Required role: ${requiredRole}`);
            }
            return func(...args);
        };
    };
}

const secureAdd = accessControlDecorator('admin')(add);

try {
    console.log(secureAdd(2, 3)); // ユーザーのロールが 'admin' でない場合、エラーがスローされる
} catch (error) {
    console.error('Caught an error:', error.message);
}

この例では、アクセス制御デコレーターを使用して、特定のロールを持つユーザーのみが関数を実行できるようにしています。

デコレーターを効果的に組み合わせることで、コードの再利用性を高め、複雑な機能を簡単に追加することができます。次のセクションでは、学んだ内容を実際に試すための演習問題を提供します。

演習問題

ここでは、デコレーターの概念をより深く理解し、実際に試してみるための演習問題を提供します。各問題に挑戦することで、デコレーターの実装方法とその応用についての知識を強化できます。

問題1: 入力検証デコレーターの作成

関数に対して、全ての引数が数値であることを検証するデコレーターを作成してください。引数に数値以外が含まれている場合はエラーをスローします。

function numberValidationDecorator(func) {
    return function(...args) {
        if (!args.every(arg => typeof arg === 'number')) {
            throw new Error('All arguments must be numbers.');
        }
        return func(...args);
    };
}

function multiply(a, b) {
    return a * b;
}

const safeMultiply = numberValidationDecorator(multiply);

console.log(safeMultiply(4, 5)); // 正常に動作
console.log(safeMultiply(4, '5')); // エラーがスローされる

問題2: 関数の実行回数制限デコレーターの作成

特定の関数の実行回数を制限するデコレーターを作成してください。例えば、関数が最大3回までしか実行されないようにします。それ以上の回数が呼び出された場合はエラーをスローします。

function limitExecutionDecorator(func, limit) {
    let count = 0;
    return function(...args) {
        if (count >= limit) {
            throw new Error(`Function ${func.name} has reached the execution limit of ${limit} times.`);
        }
        count++;
        return func(...args);
    };
}

function greet(name) {
    return `Hello, ${name}!`;
}

const limitedGreet = limitExecutionDecorator(greet, 3);

console.log(limitedGreet('Alice')); // 正常に動作
console.log(limitedGreet('Bob'));   // 正常に動作
console.log(limitedGreet('Charlie')); // 正常に動作
console.log(limitedGreet('David')); // エラーがスローされる

問題3: パフォーマンス測定デコレーターの作成

関数の実行時間を計測し、その結果をログに出力するデコレーターを作成してください。このデコレーターは、関数の実行時間が長すぎる場合に警告を表示します。

function performanceMeasureDecorator(func, threshold) {
    return function(...args) {
        const start = performance.now();
        const result = func(...args);
        const end = performance.now();
        const duration = end - start;
        console.log(`Function ${func.name} executed in ${duration} milliseconds.`);
        if (duration > threshold) {
            console.warn(`Warning: Function ${func.name} took longer than ${threshold} milliseconds.`);
        }
        return result;
    };
}

function computeSum(n) {
    let sum = 0;
    for (let i = 0; i < n; i++) {
        sum += i;
    }
    return sum;
}

const monitoredComputeSum = performanceMeasureDecorator(computeSum, 100);

console.log(monitoredComputeSum(1e6)); // 実行時間がログに記録される

問題4: エラーハンドリングとリトライデコレーターの組み合わせ

エラーハンドリングとリトライ機能を組み合わせたデコレーターを作成してください。このデコレーターは、関数がエラーをスローした場合に指定された回数だけ再試行します。

function retryDecorator(func, retries) {
    return async function(...args) {
        for (let attempt = 0; attempt <= retries; attempt++) {
            try {
                return await func(...args);
            } catch (error) {
                console.error(`Attempt ${attempt + 1} failed: ${error.message}`);
                if (attempt === retries) {
                    throw error; // 最後の試行でエラーを再スロー
                }
            }
        }
    };
}

async function fetchData(url) {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error('Network response was not ok.');
    }
    return response.json();
}

const reliableFetchData = retryDecorator(fetchData, 3);

reliableFetchData('https://jsonplaceholder.typicode.com/todos/1')
    .then(data => console.log(data))
    .catch(error => console.error('Fetch failed after 3 retries:', error.message));

これらの演習問題を通じて、デコレーターの作成と応用方法を実践的に学ぶことができます。次のセクションでは、この記事全体のまとめを行います。

まとめ

本記事では、JavaScriptにおけるデコレーターの基本概念から応用例までを詳細に解説しました。デコレーターは、関数やメソッドに追加の機能を簡単に付加する強力なツールであり、コードの再利用性と保守性を大幅に向上させます。

まず、デコレーターの基本概念とその構文、構造について学びました。次に、基本的な関数デコレーターの作成方法を紹介し、引数を持つデコレーターの作成方法も説明しました。さらに、実際の開発で役立つデコレーターの実用例を示し、ロギング機能やキャッシュ機能、エラーハンドリング機能を持つデコレーターの作成方法について詳述しました。

デコレーターの組み合わせと応用では、複数のデコレーターを適用して関数に多機能を追加する方法について学びました。演習問題を通じて、デコレーターの理解を深め、実践的なスキルを磨くことができました。

デコレーターを活用することで、JavaScriptのコードをより効率的に、そして保守しやすく設計することが可能です。今後のプロジェクトでデコレーターを活用し、コードの品質向上に役立てていただければ幸いです。

この記事が、デコレーターの理解と実装に役立つことを願っています。

コメント

コメントする

目次