Laravel Beauty: Tìm hiểu về Facade

Mở đầu

Thông qua các bài viết trước đây trong series Laravel Beauty, mình đã giới thiệu đến các bạn hai khái niệm cơ bản được sử dụng trong Laravel là Service Container và Service Provider. Đây là hai khái niệm trung tâm cần nắm vững khi muốn tìm hiểu sâu về những chức năng khác của Laravel.
Và trong bài viết lần này, ta sẽ cùng đi tìm hiểu về một tính năng chắc hẳn các bạn đã, và vẫn hay dùng trong các project của mình, và nó chỉ có thể giải thích bằng những hiểu biết về Service Container cũng như Service Provider. Thế nên nếu bạn còn chưa hiểu rõ về hai khái niệm trên thì hãy dành chút thời gian xem lại các bài viết trước trong serie Laravel Beauty này nhé.
Còn nếu bạn đã sẵn sàng thì ta hãy cùng bắt đầu nào :D

Facade là gì?

Nếu bạn thích tìm hiểu về Design Patterns, và đã từng xem qua cuốn sách nổi tiếng Design Patterns: Elements of Reusable Object-Oriented Software của "Gang of Four", thì bạn có thể sẽ biết đến Facade Pattern, nằm trong nhóm Structural.
Tuy nhiên, khái niệm Facade trong Laravel ... hoàn toàn không liên quan gì đến Facade Pattern được đề cập đến trong cuốn sách của Gang of Four nhé. =)) Giới thiệu thế để các bạn đỡ nhầm lẫn sau này thôi. =))
Vậy Facade trong Laravel là gì?
Facade có thể dịch đơn giản sang tiếng Việt là bề ngoài, mặt ngoài. Nó cho phép bạn truy cập đến các hàm bên trong các service được khai báo trong Service Container bằng cách gọi các hàm static.
Giải thích như trên thì nghe có vẻ khó hiểu, nhưng thực tế có thể bạn vẫn đang sử dụng Facade nhiều mà không để ý đấy.
Hãy cùng xem qua file config/app.php một chút nhé.
Nếu để ý ở phần cuối của file config đó, bạn sẽ thấy Laravel đã khai báo sẵn một loạt các class alias, để sau này ta có thể sử dụng trong project của mình dưới cái tên ngắn gọn, thay vì phải viết đầy đủ namespace của chúng. Tức là khi có khai báo 'Auth' => Illuminate\Support\Facades\Auth::class, thì khi chúng ta sử dụng class Auth bên trong project của mình, chúng ta thực tế đã gọi đến class Illuminate\Support\Facades\Auth.
Tản mạn một chút, nếu bạn thắc mặc tại sao Laravel có thể làm một việc tưởng chừng như "thần thánh" như vậy thì thật ra cũng chả có gì đặc biệt đâu, bởi bản thân PHP đã support cho chúng ta tính năng đó thông qua hàm class_alias rồi. Công việc của bạn bên chỉ là khai báo alias dưới dạng key-value vào trong file config/app.php, còn việc register alias thông qua hàm class_alias sẽ được Laravel thực hiện. Bạn có thể tham khảo class Illuminate\Foundation\AliasLoader để hiểu rõ bản chất của quá trình này.
Quay trở lại vấn đề register alias trong file config, nó có liên qua gì đến chủ đề Facade vậy?
Bạn có để ý tên đầy đủ của các class được register alias không? Vâng, chúng đều có namespace là Illuminate\Support\Facades. Hay nói cách khác các class được register alias ở đây đều là các Facade. Và các Facade mặc định của Laravel đều nằm trong thư mục vendor\laravel\framework\src\Illuminate\Supports.
Ngoài ra, những alias mà bạn có thể hay dùng, chẳng hạn như AppAuthDBRoute ... thì đều là Facade cả đấy.
Chẳng hạn như những dòng code quen thuộc sau:
// routes.php file
Route::get('/', function () {
    return view('welcome');
});

// Controller file
if (Auth::check()) {
    $user = Auth::user();
}
Vâng, việc bạn gọi các hàm Route::get(), hay Auth::user() như trên đều là đang sử dụng Facade. Và hãy nhìn lại phần định nghĩa mà mình viết ở phần đầu nhé. Có đúng là bạn đang sử dụng các hàm static không :D
Tuy nhiên, giờ mới là lúc vấn đề trở nên phức tạp này. Hãy thử vào xem nội dung một file Facade thế nào nhé.
Ví dụ như Auth Facade chẳng hạn. Đây là tất cả những gì bạn thấy ở class Illuminate\Support\Facades\Auth
namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Auth\AuthManager
 * @see \Illuminate\Contracts\Auth\Factory
 * @see \Illuminate\Contracts\Auth\Guard
 * @see \Illuminate\Contracts\Auth\StatefulGuard
 */
class Auth extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'auth';
    }
}
Chỉ có duy nhất 1 method trong class đó, tên là getFacadeAccessor, ngoài ra hoàn toàn không có một static method nào khác cả. Vậy thì những hàm Auth::check()Auth::user(), hay Auth::id() chui từ đâu ra?

Facade hoạt động như thế nào?

Giờ ta sẽ tiếp tục tìm hiểu ngọn ngành của một class Facade. Hãy để ý một chút, ta thấy class Auth, hay các class Facade khác, đều kế thừa từ một abstract class là Illuminate\Support\Facades\Facade. Và trong class này ta sẽ thấy có hàm getFacadeAccessor()
/**
 * Get the registered name of the component.
 *
 * @return string
 *
 * @throws \RuntimeException
 */
protected static function getFacadeAccessor()
{
    throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
Việc khai báo method với nội dung duy nhất là throw error như thế này thì class kế thừa của bạn bắt buộc phải khai báo override hàm getFacadeAccessor() (bằng không sẽ có error). Do đó tất cả các Facade đều sẽ có một static method là getFacadeAccessor() (và thật may mắn, Laravel làm hết mọi thứ cho chúng ta, làm tận răng đến mức mỗi Facade chỉ cần có một hàm đó là đủ).
Đọc kỹ nội dung abstract class Facade bạn sẽ thấy được rằng nội dung mà method getFacadeAccessor() trả về sẽ được sử dụng để tạo ra Facade Instance, mà instance này được resolve ra từ Application Instance $app, hay nói cách khác chính là Service Container.
Như vậy làm việc với Auth Facade thực tế là làm việc với service auth trong Service Container. Việc gọi các hàm static của Auth thực tế sẽ được xử lý trong magic method __callStatic (bạn có thể tìm hiểu về magic method này ở đây), rồi chuyển qua lời gọi hàm bình thường từ một instance đã được resolve ra từ trong Service Container.
Như vậy thì ta có thể thấy các cách gọi sau sẽ là tương đương:
Auth::check();

// Bản chất của lời gọi hàm static qua Facade có thể được tóm tắt lại thành
$auth = app('auth');
$auth->check();

Facade - the Good and the Bad

Facade có thể nói là một trong những khái niệm hay tính năng gây tranh cãi nhiều nhất của Laravel. Bạn có thể google ra cả đống bài viết, ý kiến cho rằng Facade là một "Bad design", hay "Anti-pattern". Nhưng cũng có nhiều người vẫn yêu thích và sử dụng Facade hàng ngày. Nó đã và vẫn đang là một phần của Laravel.
Facade thuận tiện. Facade dễ dùng. Facade giúp bạn viết code rất ngắn gọn nhanh chóng. Trên document của Laravel, Facade được miêu tả với rất nhiều ưu điểm như "providing the benefit of a terse, expressive syntax while maintaining more testability and flexibility than traditional static methods".
Facade được thiết kế như một cây cầu để người lập trình tiếp cận với "Service" một cách dễ dàng. Bạn có thể sử dụng Facade gần như mọi lúc mọi nơi mà không phải mất công khởi tạo, resolve các instance từ trong Service Container. Facade về bản chất cũng không phải là một class chứa đầy những static method, nên nó không nặng nề chiếm bộ nhớ như những class như vậy.
Tuy nhiên chính vì để có được những sự tiện lợi như thế mà ta cần một đống các "phép thuật" được xử lý ngầm "behind the scene". Với một người mới bắt đầu tìm hiểu và sử dụng Laravel, có thể họ sẽ dễ dàng biết đến và sử dụng cách gọi Auth::user(), nhưng họ sẽ không thể hiểu được bản chất của hàm đó từ đâu ra. Hay khi gặp vấn đề và cần vào trong code của Framework để kiểm tra thì cũng thật khó để bạn tìm thấy được nguồn gốc của chúng.
Hơn thế nữa, với việc quá tiện dụng của Facade, nếu không cẩn thận bạn sẽ có thể làm code trở nên phức tạp và khó đoán biết hơn. Ví dụ như mình muốn kiểm tra một class hoạt động dựa vào các dependencies nào thì mình thường sẽ xem ở trong constructor của nó. Hay trong Laravel có support method injection thì mình cũng xem qua cả trong khai báo method nữa. Thế nhưng, với việc gọi Facade ở bên trong hàm thì ta đã vô hình chung che đi việc hàm, hay class, đó cần một service dependency, thứ mà thay vì ta nên inject vào thì nay được resolve ra thông qua Facade.

Lời kết

Trong khuôn khổ nội dung bài viết này, mình xin đứng ngoài cuộc về việc tranh cãi xem nên hay không nên sử dụng Facade. Nó là một tính năng tiện dụng và không phải ngẫu nhiên mà Taylor Otwell, cha đẻ của Laravel, còn giữ lại nó đến ngày hôm nay.
Bài viết này chỉ hy vọng có thể giúp các bạn có thể hiểu hơn về những gì xảy ra đằng sau những hàm static mà ta vẫn hay gọi thông qua Facade.
Còn với câu hỏi "Nếu không dùng Facade thì có thể dùng cái gì để thay thế?", thì mình xin trả lời là mình vẫn hay dùng Dependency Injection.
Hãy dành chút thời gian tham khảo về phần Facade Class Reference trên document của Laravel. Bạn sẽ thấy được bản chất của một Facade là sử dụng instance của class nào, và nếu khi nào có vấn đề gì cần phải xem code của Framework thì cũng biết được nơi nào để mà tìm. Chẳng hạn như để tìm hiểu về các hàm của Facade Request thì bạn nên tìm đến class Illuminate\Http\Request, với Auth thì là Illuminate\Auth\AuthManager hay Mail thì là Illuminate\Mail\Mailer.
Và như vậy thì ta có thể thay thế việc sử dụng Facade Mail bằng cách inject
Illuminate\Mail\Mailer vào class thông qua constructor injection hay method injection.
Có một chú ý ở đây là như mình cũng đã có đề cập ở các bài trước, một class không nên để phụ thuộc vào một class khác mà chỉ nên phụ thuộc vào abstraction (hay interface). Do đó, thay vì typehint Illuminate\Mail\Mailer ta sẽ typehint contract của nó là Illuminate\Contracts\Mail\Mailer
// Facade Style
function sendMail()
{
    // Sending mail
    Mail::send($view, $data);
}

// Dependency Injection Style
class Something
{
    protected $mailer;

    public function __construct(Illuminate\Contracts\Mail\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function sendMail()
    {
        // Sending mail
        $this->mailer->send($view, $data);
    }
}
Hy vọng qua bài viết này mọi người có thể hiểu thêm về một tính năng vốn rất quen thuộc của Laravel. Và xin hẹn gặp lại vào bài viết tới, với những đào sâu khác về Framework phổ biến này :)

Comments

Popular Posts