+23

Kiến trúc hệ thống trên Laravel – phần 3

Các bài viết trong series

Kiến trúc hệ thống trên Laravel – phần 1 : Tại sao phải áp dụng architect vào trong Laravel Kiến trúc hệ thống trên Laravel – phần 2 : OOP, Interface, Dependency Injection, IoC Kiến trúc hệ thống trên Laravel – phần 3 : Phân tích sâu vào việc sử dụng interface Kiến trúc hệ thống trên Laravel – phần 4 : Design Pattern – Decorator Kiến trúc hệ thống trên Laravel – phần 5 : Design Pattern – Adapter Kiến trúc hệ thống trên Laravel – phần 6 : Design Pattern – Repository Kiến trúc hệ thống trên Laravel – phần 7 : Design Pattern – Factory Kiến trúc hệ thống trên Laravel – phần 8 : Advance component trong Laravel Kiến trúc hệ thống trên Laravel – phần 9 : Mô hình kiến trúc cụ thể Part 1 Kiến trúc hệ thống trên Laravel – phần 10 : Mô hình kiến trúc cụ thể Part 2

Xin chào các bạn. Mình vào nghề lập trình cũng đã lâu, cũng có 1 số hiểu biết coi như là nâng cao về framework Laravel. Nên hôm nay mình xin chia sẻ 1 chút về kiến trúc hệ thống của mình được xây dựng trên Laravel như thế nào. Mong rằng có thể giúp ích cho các bạn 😃.


Hi, chúng ta lại quay lại series nhé. Mình có nhận được nhiều feedback của mọi người. Trước hết là xin chân thành cảm ơn các bạn đã đóng góp ý kiến. Rồi, chúng ta vào vấn đề chính nhá.

Nhắc lại về interface 1 chút nhé

Bạn có 1 cái laptop với 8Gb Ram hàn chết trên bo mạch (Mac Book 2016 ^^). Một ngày đẹp trời, bạn cần nâng cấp RAM lên 16Gb -> toi rồi, 1 là bạn mang ra thợ, gỡ mối hàn ra, mua ram mới rồi hàn lại (ai biết sau khi hàn lại nó có làm toi cái laptop của bạn không), 2 là bạn mua laptop mới với 16Gb Ram với giá đắt gấp đôi

Bạn có 1 cái laptop với 8Gb Ram được kết nối thông qua khe cắm Ram trên board mạch -> bạn cần nâng cấp Ram lên 16Gb -> đơn giản, mua ram mới, tháo thanh ram cũ ra, gắn thanh mới vào. Máy tính lại chạy ngon và mạnh hơn lúc đầu ^^.

Bạn code 1 hệ thống có chức năng gửi email, bạn hardcode chức năng đó vào khắp mọi nơi -> Một ngày đẹp trời, bạn cần bổ sung chức năng nhắn sms bên cạnh gửi email -> thôi toi rồi, bạn có 2 cách để giải quyết: 1, sửa tất cả những chỗ hardcode để thêm tính năng, 2 là đập đi làm lại từ đầu ^^

Bạn có 1 hệ thống với chức năng gửi email kết nối vào hệ thống thông qua 1 interface -> cần thêm tính năng nhắn sms bên cạnh gửi email, no problem, bạn viết 1 class mới bao hàm cả gửi mail và sms rồi binding lại interface vào class đó -> xong, hệ thống lại chạy ro ro ^^.

Khi nào cần sử dụng interface: mọi lúc có thể ^^. Nhưng đặc biệt là khi bạn** sử dụng package ngoài **vào thì bạn phải sử dụng ngay interface

Ví dụ nào

Há há, chúng ta tiếp tục ví dụ ở phần trước nhá để mình nhắc lại nhé:

Mình có 1 interface Notify để gửi thông tin ra ngoài cho user Mình có 1 class LaravelMailer để bind interface đó để sử dụng Laravel Email

use Mail;

class LaravelMailer implements Notify
{
	public function send($subject, $template, $data)
	{
		 Mail::send($template, ['user' => $data], function ($m) use ($data) {
                     $m->from('hello@app.com', 'Your Application');

                     $m->to($data->email, $data->name)->subject($subject);
                 });
	}
}

Ok, giờ muốn thêm chức năng sms bên cạnh chức năng email thì làm thế nào thì làm thế nào

Đầu tiên bạn kiếm 1 package hỗ trợ chức năng gửi sms rồi nhúng package đó vào trong 1 class mới để sử dụng cùng với send email

Mình sẽ chỉ cho bạn 2 cách làm: 1 cách đơn giản nhưng không linh hoạt và 1 cách phức tạp nhưng linh hoạt hơn

Cách 1: Cách đơn giản nhưng kém linh hoạt:

Tạo 1 class mới và copy nguyên phần Laravel Email + thêm code cho phần SMS

use Mail;

class MailAndSms implements Notify
{
	public function send($subject, $template, $data)
	{
                 // Copy nguyên hàm send email ở phần trước
		 Mail::send($template, ['user' => $data], function ($m) use ($data) {
                     $m->from('hello@app.com', 'Your Application');

                     $m->to($data->email, $data->name)->subject($subject);
                 });

                 // Thêm code tích hợp SMS
                 SmsPackage::send(...)
	}
}

Rồi binding lại interface Notify vào MailAndSms

use Illuminate\Support\ServiceProvider;

class NotifyServiceProvider extends ServiceProvider
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('Notify', function ($app) {
            // return new LaravelEmail();
            return new MailAndSms();
        });
    }
}

Cách này đã giải quyết được bài toán của chúng ta nhé, hiện tại chúng ta sẽ có 2 biến thể của Notify là LaravelMailer và MailAndSms -> bạn có thể linh hoạt chuyển đổi để lựa chọn biến thể nào sử dụng cho hệ thống.

Nhưng vì chúng ta copy nguyên code của LaravelMailer vào trong MailAndSms nên nếu chức năng gửi email cần phải update trong nội tại của nó thì chúng ta vẫn phải sửa ở cả LaravelMailer và MailAndSms -> về nguyên tắc tuyệt đối không được copy code (Don’t repeat your-self).

Cách 2: Interface lồng interface

Chúng ta đã có interface Notify cho hệ thống chính, nhưng phần class cụ thể thực thi interface này lại sử dụng các component khác nhau. Để các component này được linh hoạt, chúng ta cũng đi định nghĩa lại interface cho từng phần rồi sử dụng trong hệ thống chính cũng như trong các hệ thống con liên kết.

Định nghĩa interface cho việc gửi mail

interface MailAdapter
{
    public function send(...);
}

Định nghĩa interface cho việc gửi sms

interface SmsAdapter
{
    public function send(...);
}

Viết lại class LaravelMailer để thực thi MailAdapter

use Mail;

class LaravelMailer implements MailAdapter
{
	public function send($subject, $template, $data)
	{
		 Mail::send($template, ['user' => $data], function ($m) use ($data) {
                $m->from('hello@app.com', 'Your Application');

                $m->to($data->email, $data->name)->subject($subject);
         });
	}
}

Thêm mới class Sms để thực hiện SmsAdapter

class Sms implements SmsAdapter
{
	public function send($subject, $template, $data)
	{
		 SmsPackage::send(...)
	}
}

Thêm mới class để thực thi việc chỉ gửi email

<?php

class EmailOnly implements Notify
{
    protected $mailer;

    public function __construct(MailAdapter $mailer)
    {
        $this->mailer = $mailer;
    }

    public function send($subject, $template, $data)
    {
        $this->mailer->send($subject, $template, $data);
    }
}

Viết lại class MailAndSms để sử dụng MailAdapter và SmsAdapter

<?php

class MailAndSms implements Notify
{
    protected $mailer;
    protected $sms;

    public function __construct(MailAdapter $mailer,SmsAdapter $sms)
    {
        $this->mailer = $mailer;
        $this->sms = $sms;
    }

    public function send($subject, $template, $data)
    {
        $this->mailer->send($subject, $template, $data);
        $this->sms->send($subject, $template, $data);
    }
}

Viết lại Service Provider để binding các interface vào các class tương ứng:

use Illuminate\Support\ServiceProvider;

class NotifyServiceProvider extends ServiceProvider
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('MailAdapter', function ($app) {
            return new LaravelMailer();
        });
        $this->app->singleton('SmsAdapter', function ($app) {
            return new Sms();
        });
        $this->app->singleton('Notify', function ($app) {
            // return new EmailOnly(); //Nếu muốn gửi email thôi thì uncomment dòng này và comment dòng dưới
            return new MailAndSms();
        });
    }
}

Rồi, cách thứ 2 đã hoàn thành, với cách này thì mình đã bôi ra rất nhiều thứ nhưng có 1 số điểm các bạn nên lưu ý:

  • Hệ thống chính sử dụng interface Notify. Nên dù mình có bôi ra phần xử lý phía trong thì code ở Controller khi sử dụng Notify hoàn toàn không thay đổi gì -> code ở phần 2 thế nào thì nó vẫn nguyên thế 😄.
  • Các biến thể của Notify (EmailOnly và MailAndSms) lại sử dụng các interface cho các component con (EmailAdapter và SmsAdapter) -> vì cách code này nên các hàm thực thi task vụ cụ thể (ví dụ như gửi email, hay là gửi sms) chỉ cần định nghĩa 1 lần và được sử dụng ở rất nhiều nơi -> giờ anh em có cần update gì thì chỉ cần update 1 chỗ
  • Với cách viết này, nếu bạn cần thêm 1 chức năng mới (gửi lên Slack – chẳng hạn như thế) thì bạn hoàn toàn không phải động vào code xử lý cũ mà chỉ thay đổi phần binding. Cụ thể, bạn cần viết interface mới (SlackAdapter) và class thực thi interface đó -> thêm 1 class mới EmailAndSmsAndSlack sử dụng thêm SlackAdapter và sửa lại Service Provider để binding Notify vào EmailAndSmsAndSlack là xong
class MailAndSmsAndSlack implements Notify
{
    protected $mailer;
    protected $sms;
    protected $slack;

    public function __construct(MailAdapter $mailer,SmsAdapter $sms,SlackAdapter $slack)
    {
        $this->mailer = $mailer;
        $this->sms = $sms;
        $this->slack = $slack;
    }

    public function send($subject, $template, $data)
    {
        $this->mailer->send($subject, $template, $data);
        $this->sms->send($subject, $template, $data);
        $this->slack->send($subject, $template, $data);
    }
}

Chú ý: MailAndSmsAndSlack đang copy nguyên code của MailAndSms và thêm code phần slack vào (vi phạm don’t repeat your self) -> sẽ có cách để khắc phục vấn đề này, mời các bạn theo dõi trong bài tới. Xin chân thành cảm ơn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí