はじめに
この記事はプログラミング初学者による備忘録用の記事であり、少しでも他の初学者のお役に立てればと思い書いています。
今回は、Laravelのファサードの仕組みが気になったので少し調べてみました。
便利な機能ですが、使い方次第ではクラスの責任範囲が大きくなりすぎる可能性があるので、基本的にファサードを使わない実装が可能であれば使わない方が良いのかもしれません。
間違いなどがございましたら、ご指摘のほどよろしくお願い致します。
ファサードとは
公式ドキュメントによると、ファサードは以下のように記述されています。
ファサードは、アプリケーションのサービスコンテナで使用可能なクラスに対して「静的な」インターフェイスを提供します。
Laravelは、Laravelのほとんどすべての機能へのアクセスを提供する多くのファサードを提供しています。
Laravelファサードは、サービスコンテナ内の基礎となるクラスへの「静的プロキシ」として機能し、従来の静的メソッドよりもテスト容易性と柔軟性を維持しながら、簡潔で表現力豊かな構文という利点を提供しています。
引用:Laravel 8.x ファサード
簡単に言うと、ファサードとはクラスをインスタンス化せずにメソッドを実行可能にする機能です。
予め備わっているファサードクラスや、自作したファサードを使用することができます。
以下のコードは、自作したファサードの簡易的な使用例です。
ファサードクラス名::メソッド名();のみで、特定のメソッドを使用することができます。
// 通常はインスタンス化してメソッドを実行
$classA = new ClassA();
$classA->methodA();
// ファサードを利用すると、以下のようにインスタンス化せずにメソッドを利用することができる(裏側でインスタンス化されている)
FacadeClassA::methodA();
ファサードの危険性
ファサードを利用する場合、インスタンス生成や、どのクラスに依存しているのかなどを考えることが少ないかもしれません。
それ故に、複雑になり易く、クラスの責任範囲が大きくなってしまう可能性があります。
ファサードを使用する際に、起こりうるデメリットとして、主に以下の3点が挙げられます。
- クラス同士の依存関係が不透明になる可能性がある
- ファサードを使うことで、テストの実行が難しくなる可能性がある
- コードの再利用性が低くなる可能性がある
公式ドキュメントでも、以下のような注意喚起が記されています。
ただし、ファサードを使用する場合は注意が必要です。ファサードの主な危険性は、クラスの「スコープクリープ」です。ファサードは非常に使いやすく、依存注入を必要としないため、1つのクラスで多くのファサードを使用するのは簡単で、クラスを成長させ続けてしまいがちです。依存注入を使用していれば、大きなコンストラクタによりクラスが大きくなりすぎていることを示す視覚的なフィードバックにより、これが起きる可能性は低減されます。したがって、ファサードを使用するときは、クラスのサイズに特に注意して、クラスの責任範囲が狭くなるようにしてください。クラスが大きくなりすぎている場合は、クラスを複数の小さなクラスに分割することを検討してください。
引用:Laravel8.x ファサード
従って、ファサードを使用する際は、長期的に運用、保守していく上で本当に使用しても良いのか、開発チーム等で話し合うべきだと思います。
ファサードの作成
実際にファサードを作成し、仕組みを追ってみたいと思います。
1.メソッドを持つクラスを作成(ファサードを利用してメソッドを実行する対象となるクラス)
# 略
class ClassA
{
public function methodA()
{
return 'methodA';
}
}
今回は、上記のメソッドを以下のようにファサードを使って呼び出せるようにします。
\FacadeClassA::methodA();
2.サービスコンテナに登録する
ファサードとして利用するクラスは、サービスプロバイダのregisterメソッド内でサービスコンテナに登録します。
bind()の第一引数には、特定の結合名を指定してください。第二引数には、ファサードとして利用するクラスを指定します。
public function register()
{
$this->app()->bind('classA', ClassA::class);
}
3.ファサードクラスの作成
次に、ファサードクラスを作成します。
ファサードクラスを作成する際は、以下のポイントに気をつけてください。
-
Illuminate\Support\Facades\Facadeをextendsする -
getFacadeAccessor()メソッドを定義する -
getFacadeAccessor()メソッド内で、サービスコンテナに登録してある結合名を返すように設定する
use Illuminate\Support\Facades\Facade;
class FacadeClassA extends Facade
{
protected static function getFacadeAccessor()
{
return 'classA';
}
}
4.エイリアスに登録する
エイリアスに登録することで、ファサードを呼び出す際にuse App\Facades\FacadeClassA;といった記述が不要になります。
ファサードを呼び出す際は、先頭に\を付けて\FacadeClassA::methodA();といったように呼び出すことができます。
'aliases' => [
'FacadeClassA' => \App\Facades\FacadeClassA::class,
]
以上で、\FacadeClassA::methodA();といったように呼び出すことができます。
ファサードの仕組みについて
今回、以下のようなファサードクラスを作成しました。
このクラスには、\FacadeClassA::methodA();で呼び出されているmethodAメソッドが存在しません。
代わりに、getFacadeAccessorメソッドが存在しています。
これ以上、情報がないので親クラスであるIlluminate\Support\Facades\Facade;を追ってみると何かが分かりそうな気がします。
use Illuminate\Support\Facades\Facade;
class FacadeClassA extends Facade
{
protected static function getFacadeAccessor()
{
return 'classA';
}
}
・ Illuminate\Support\Facades\Facade;を追ってみる
では、先ほど述べたように、Illuminate\Support\Facades\Facade;を調べてみます。
Facade.php内に、__callStatic()というマジックメソッドが確認できると思います。
# 略
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
return $instance->$method(...$args);
}
この__callStatic()というマジックメソッドは、アクセス不能メソッドを静的コンテキストで実行した時に呼び出されます。
引数$methodには、 コールしようとしたメソッドの名前が入り、引数$argsには、メソッド($method)に渡そうとしたパラメータが配列で格納されます。
つまり、今回はFacadeClassAに存在しないmethodA()をstaticに呼び出そうとしたので、マジックメソッドである__callStatic()が実行されることになります。
__callStatic()の中には、$instance = static::getFacadeRoot();と、 return $instance->$method(...$args);が記述されており、次はこのコードを読み解くとファサードの仕組みが分かりそうです。
・ static::getFacadeRoot();を読み解く
このメソッドでは、FacadeClassAに実装したgetFacadeAccessor()が実行されています。
#略
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
作成したファサードクラスのgetFacadeAccessor()では、returnで文字列classAを返すようにしているので、getFacadeRoot()内のstatic::getFacadeAccessor()では、実際には以下のような働きをしています。
#略
public static function getFacadeRoot()
{
return static::resolveFacadeInstance('classA');
}
次に、static::resolveFacadeInstance()を読み解くとファサードの仕組みが分かりそうです。
・ static::resolveFacadeInstance()を読み解く
このメソッドでは、サービスコンテナを利用して対象のクラスをインスタンス化して返しています。
今回は、$nameに先ほどの文字列classAが入っているので、以下のような役割を果たしています。
static::$app[$name]; = return app()->make('classA');
#略
protected static function resolveFacadeInstance($name)
{
return static::$app[$name];
}
以上を踏まえて、マジックメソッド__callStatic()をもう一度見てください。
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot(); =>対象のクラスをインスタンス化
return $instance->$method(...$args); =>インスタンス化したクラスのメソッドを実行
}
上記のように、$instance = static::getFacadeRoot();には、特定のクラスのインスタンスが入っており、return $instance->$method(...$args);で特定のクラスのメソッドを実行していることが分かると思います。
まとめ
今回、作成したファサードクラスでは、マジックメソッドである__callStaticが呼び出され、getFacadeRoot()内のgetFacadeAccessor()で定義したサービスコンテナに登録してある結合名を使用して特定のクラスをインスタンス化し、$instance->$method(...$args);でそのインスタンス化したクラスのメソッドを実行していることがわかりました。
ファサードは何かと便利な機能ですが、使用するとクラスの責任範囲が広がる可能性があるので、使用する際はクラスの責任範囲に気をつかう必要がありそうです。