Symfonyのほうから来ましたこんにちは。
長年「宗教上の理由でLaravelには入門しません」と言っていた私ですが、 副業 でLaravelで作られたアプリケーションに関わることになり、とうとうLaravelを触り始めました。
悪名高きFacade
私がLaravelに入門したことがなくても名前を知っていたのが Facade
です。
カンファレンスや勉強会で知り合った範囲のLaravelユーザー達が口を揃えて「Facadeはよくない」と言っていたので、「なんかしらんけど良くないもの」として名前だけ認識していました。
Laravel製アプリケーションに関わることになって、ソースコードをgit cloneして最初に調べたのが app/
配下のuse文です。
use Illuminate\Support\Facades
を検索すると、あー…あるある…。
Facadeの何が良くないか?
これはLaravelの上級者達によって既にあちこちで語られているので、「Facade よくない」とかで検索してみてください。
私はドメインのコード内に記載することでLaravelへの暗黙的な依存が発生するのが一番良くないと思います。
Laravel側のアップデートや仕様変更によってドメインのコードが左右されると、アプリケーションのメンテナンスしやすさが下がります。はるか昔のバージョンからずっとフレームワークをバージョンアップできない事になりかねません。
また、Facadeが使われているクラスのユニットテストをしたいときに、LaravelのDIコンテナの初期化が必須になるのもマイナスです。
テストの実行にかかる所要時間が伸びると、テストが億劫になり、テストを避ける(ちょっとした仕様変更の際にテストを省略しようとする、手元でテストしない、「急いでるのでCI落ちてますがマージお願いします!」)悪い習慣がついてしまいます。
継続的にテストしていくために、テストは「速く」実行できることがとても重要です。
IDEで補完されない(これはプラグインとかで解決できるのかも?)というのも地味に面倒な点です。
Facadeは何をしているのか?(仕組み)
「良くないもの」と再認識したFacadeを取り除いていくためには、まずFacadeが何をしているのか理解した上で、Facadeを使わない書き方を考えていく必要があります。
オープンソースを使うメリットはソースコードが見れることですから、Facadeのソースコードを見てみましょう。
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/Facades/Facade.php
ソースコードを見ると、Facadeを使って DB::hoge()
のようなことをした場合、まず呼び出されるのはFacade::__callStatic()
です。
abstract class Facade
{
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
getFacadeRoot()
によりFacadeの対象のインスタンスを取り出し、そのインスタンスに対してstaticコールされたメソッドを実行していることがわかります。
ではFacadeの対象インスタンスをどのように取り出しているのかと言うと…
abstract class Facade
{
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
}
}
getFacadeAccessor()
により対象のサービス名を特定した上で、 static::$app
(DIコンテナ)からその名前のサービスを取得しています。
つまり、
class Hoge extends Facade
{
protected static function getFacadeAccessor()
{
return 'hoge';
}
}
のようなFacadeがあって
Hoge::foo($bar);
のように呼び出されているのであれば、DIコンテナ上で hoge
という名前で定義されているサービスのクラス名を調べて、autowireしてやれば同等のことができるというわけです。
- Hoge::foo($bar)
+ $this->hoge->foo($bar);
IDEの補完もきくし、テスト時にピュアなPHPUnitのTestCaseクラスでテストが書けます。クリーン
まとめ
アプリケーション内のソースコード内を概観して、Facadeが使われているのは AOP がマッチしそうなケースだなーと思いました。
DBとかRedisとか使っちゃってるのはだめだぞ☆って感じですけども。
ごく一般的なLaravel製アプリケーションを、メンテ性高く長期運用できるシステムへ改善する過程を一緒に体験してみたい(一緒に苦労してみたいとも言う…)方がいたら https://www.wantedly.com/projects/475929 からご連絡くださいね
もしこれを読んで、最初からFacadeがなくてすべてがDIされている世界(Symfony)に興味を持った方は Symfony Advent Calendar 2020 も覗いてみてください