初めに
最近読んだ関数型プログラミングに関する本の中で、関数を別の関数の引数として渡すことの有用性を知りました。PHPでもarray_map
やarray_find
などの配列操作関数で同様な実装がされており、より汎用的な関数を書くことが可能です。
この記事では、PHPにおけるクロージャ(無名関数)を引数にとる関数のメリットなどについて解説します。
クロージャを引数にとる関数
Closure(無名関数)はPHPの型の一つです。
クロージャは関数の引数や戻り値として定義・利用することができます。
以下はクロージャを引数に用いた関数の例です。
<?php
// 営業時間内かどうかを調べる関数
function isDuringBusinessHours(Closure $getNow, Closure $onSuccess, Closure $onError) {
$now = $getNow();
$start = new DateTime('10:00');
$end = new DateTime('18:00');
if ($now >= $start && $now < $end) {
// 営業時間内の処理
$onSuccess();
} else {
// 営業時間外の処理
$onError();
}
};
$getNow = function(): Datetime {
return new DateTime('now');
};
$echoSuccess = function(): void {
echo '営業時間内です';
};
$echoError = function(): void {
echo '営業時間外です';
};
// クロージャを渡して関数を呼びだす
isDuringBusinessHours($getNow, $echoSuccess, $echoError);
この例では、isDuringBusinessHours()
関数が3つのクロージャを引数として受け取り、その内部で呼び出しています。呼び出す側は、具体的な処理内容をクロージャとして定義し、引数として渡すだけで済みます。
これにより、条件に応じた具体的な処理を呼び出す側で決めることができ、関数をより汎用的に使うことができます。
配列操作の組み込み関数
ご存じの通り、PHPには配列を操作する多くの組み込み関数があり、その多くでクロージャが用いられています。
例えば、array_find
では、第二引数でクロージャを指定することで、条件を自由に変更することが可能です。
<?php
$array = [
'a' => 'dog',
'b' => 'cat',
'c' => 'cow',
'd' => 'duck',
'e' => 'goose',
'f' => 'elephant'
];
// 名前が4文字より長い最初の動物を探します。
var_dump(array_find($array, function (string $value) {
return strlen($value) > 4;
}));
array_find
は渡されたクロージャがどのような実装をしているかを知る必要がありません。これを見ると、クロージャを引数にとる関数が汎用的であることがよく分かると思います。
クロージャを引数として利用する具体的なメリット
クロージャを引数として利用することによる利点はいくつかあります。
- 関数の前後に処理を差し込める(デコレーターパターン風)
- 単体テストしやす実装になる
- オープン・クローズドの原則に沿った実装になる
1. 関数の前後に処理を差し込める(デコレーターパターン風)
例えば、関数を呼び出す前後でログを出力したいとします。
単純にログ出力を挟むだけでは、同じようなコードが色々な場所に散らばってしまいます。
$myOperation = function(int $a, int $b): int {
return $a + $b;
};
echo "関数 myOperation の実行を開始します.";
$withLogging($myOperation, 'myOperation');
echo "関数 myOperation の実行を完了しました.";
そこで、より汎用的にできるよう、以下のようにリファクタリングしました。
function withLogging(Closure $func, string $funcName): Closure
{
return function(...$args) use ($func, $funcName) {
echo "関数 {$funcName} の実行を開始します.";
$result = $func(...$args);
echo "関数 {$funcName} の実行を完了しました.";
return $result;
};
}
$myOperation = function(int $a, int $b): int {
return $a + $b;
};
$loggedOperation = withLogging($myOperation, 'myOperation');
withLogging()
関数は引数でクロージャを受け取り、実行の前後でログ出力を行います。
これにより、withLogging()
はどんな関数でも利用できる「デコレーターパターン風」な汎用的関数になります。
2. 単体テストしやす実装になる
クロージャを引数に用いることで、単体テスト容易性を高めることができます。
例えば、以下のisDuringBusinessHours()
は、メソッド内で現在時刻を取得しています。そのため、テストを実行するタイミングによって結果が変わってしまうフレーキーなテストになってしまいます。
// 営業時間内かどうかを調べる関数
function isDuringBusinessHours() {
// 実行タイミングによって取得できる時間が異なる
$now = new DateTime('now');
$start = new DateTime('10:00');
$end = new DateTime('18:00');
return ($now >= $start && $now < $end)
};
それに対して、時刻を取得する処理をクロージャとして定義し、引数で受け取るように変更しました。
// 営業時間内かどうかを調べる関数
function isDuringBusinessHours(Closure $getNow) {
$now = $getNow();
$start = new DateTime('10:00');
$end = new DateTime('18:00');
return ($now >= $start && $now < $end)
}
isDuringBusinessHours(
function() {
// 好きな時間を設定できる
return new DateTime('15:00');
},
)
こうすることにより、テスト実行時に好きな時間を決めることができるため、テストがしやすくなります。
3. オープン・クローズドの原則に沿った実装になる
デザイン原則の一つである**オープン・クローズドの原則 (Open/Closed Principle)**は、「拡張に対しては開かれており、修正に対しては閉じているべき」というものです。
例えば、以下のような価格計算のロジックがあるとします。
<?php
class Calculator
{
/**
* 価格計算
*/
public function calculate(float $amount, string $taxType): float
{
switch ($taxType) {
case 'normal':
return $amount * 1.10; // 10% 消費税
case 'reduced':
return $amount * 1.08; // 8% 軽減税率
default:
throw new InvalidArgumentException("不明な税率タイプ: " . $taxType);
}
}
}
$calculator = new Calculator();
echo "通常税率: " . $calculator->calculate(1000, 'normal');
echo "軽減税率: " . $calculator->calculate(1000, 'reduced');
もし、他の計算方法が追加されるとした場合,calcutate()
に修正が入ります。
これはオープン・クローズドの原則に反し、変更に弱いコードになってしまいます。
そこで、calcurate()
の引数にクロージャを追加し、呼び出し元で計算ロジックを決めるように変更しました。
class Calculator
{
public function calculate(float $amount, Closure $strategy): float
{
return $strategy($amount);
}
}
$calculator = new Calculator();
// 通常の税率
$normalTaxStrategy = function(float $amount): float {
return $amount * 1.10; // 10% 消費税
};
// 軽減税率
$reducedTaxStrategy = function(float $amount): float {
return $amount * 1.08; // 8% 軽減税率
};
echo "通常税率: " . $calculator->calculate(1000, $normalTaxStrategy); // 1100
echo "軽減税率: " . $calculator->calculate(1000, $reducedTaxStrategy); // 1080
こうすることで、他の計算方法が追加される場合でもcalculate()
のロジック変更がなく、オープン・クローズド原則に沿った実装にすることができます。
終わりに
クロージャを関数の引数として利用することは、PHPでより汎用的で再利用性の高い関数を書き、システムの柔軟性や保守性を向上させるための強力な手段です。ぜひ、日々のコーディングでクロージャの活用を検討してみてはいかがでしょうか。
ここまでご覧いただきありがとうございました!