Laravel Container (容器) 概念詳解 (上)

Ίκαρος發表於2017-09-19

本文翻譯自 Symfony 作者 Fabien Potencier 的 《Dependency Injection in general and the implementation of a Dependency Injection Container in PHP》 系列文章。

專有名詞翻譯成中文後會變得不利於理解,後續文章中將改用括號+中文備註的形式。

上文我透過一些示例講解了 Dependency Injection ,本文將接著介紹 Dependency Injection Containers (容器) 的概念。

首先記住這句話:

大多數時候,Dependency Injection 並不需要 Container

只有當你需要管理一大堆具有很多依賴關係的不同物件時,Container 才會非常有用(例如框架中)。

上文書,建立 User 物件需要先建立 SessionStorate 物件。這裡的有個瑕疵,建立物件時需要提前知道它所有的依賴項:

$storage = new SessionStorage('SESSION_ID');
$user = new User($storage);

Zend FrameworkZend_Mail 庫傳送郵件過程為例:

$transport = new Zend_Mail_Transport_Smtp('smtp.gmail.com', [
  'auth'     => 'login',
  'username' => 'foo',
  'password' => 'bar',
  'ssl'      => 'ssl',
  'port'     => 465,
]);

$mailer = new Zend_Mail();
$mailer->setDefaultTransport($transport);

請把這個例子看做一個大系統中的一小部分,因為這種簡單的例子當然沒必要用 Container

Dependency Injection Container 是一個“知道如何例項化和配置物件”的物件(工廠模式的昇華)。為了做到這點,它需要知道建構函式的引數、以及物件之間的關係。

下面是一個寫死 Zend_MailContainer

class Container
{
  public function getMailTransport()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', [
      'auth'     => 'login',
      'username' => 'foo',
      'password' => 'bar',
      'ssl'      => 'ssl',
      'port'     => 465,
    ]);
  }

  public function getMailer()
  {
    $mailer = new Zend_Mail();
    $mailer->setDefaultTransport($this->getMailTransport());

    return $mailer;
  }
}

這個 Container 用起來就相當簡單了:

$container = new Container();
$mailer = $container->getMailer();

我們只管向 Containermailer 物件就行,完全不用管 mailer 怎麼建立。建立 mailer 物件的“雜活”是嵌入在 Container 中的。
Container 透過 getMailTransport() 方法,把 Zend_Mail_Transport_Smtp 這個依賴自動注入到了 Zend_Mail 中。

細心的網友可能已經發現,這裡的 Container 把什麼都寫死了。我們可以完善一下:

class Container
{
  protected $parameters = array();

  public function __construct(array $parameters = [])
  {
    $this->parameters = $parameters;
  }

  public function getMailTransport()
  {
    return new Zend_Mail_Transport_Smtp('smtp.gmail.com', [
      'auth'     => 'login',
      'username' => $this->parameters['mailer.username'],
      'password' => $this->parameters['mailer.password'],
      'ssl'      => 'ssl',
      'port'     => 465,
    ]);
  }

  public function getMailer()
  {
    $mailer = new Zend_Mail();
    $mailer->setDefaultTransport($this->getMailTransport());

    return $mailer;
  }
}

現在就可以隨時更改 usernamepassword 了:

$container = new Container([
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
]);
$mailer = $container->getMailer();

如果需要更改 mailer 類,把類名也當引數傳入就行:

class Container
{
  // ...

  public function getMailer()
  {
    $class = $this->parameters['mailer.class'];

    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransport());

    return $mailer;
  }
}

$container = new Container([
  'mailer.username' => 'foo',
  'mailer.password' => 'bar',
  'mailer.class'    => 'Zend_Mail',
]);
$mailer = $container->getMailer();

如果想每次獲取同一個 mailer 例項,可以用 單例模式

class Container
{
  static protected $shared = [];

  // ...

  public function getMailer()
  {
    if (isset(self::$shared['mailer']))
    {
      return self::$shared['mailer'];
    }

    $class = $this->parameters['mailer.class'];

    $mailer = new $class();
    $mailer->setDefaultTransport($this->getMailTransport());

    return self::$shared['mailer'] = $mailer;
  }
}

這就包含了 Dependency Injection Containers 的基本功能:

  • Container 管理物件例項化到配置的過程
  • 物件本身不知道自己是由 Container 管理的,對 Container 一無所知。

這就是為什麼 Container 能夠管理任何 PHP 物件。 物件使用 DI 來管理依賴關係非常好,但不是必須的。

Container 很容易實現,但手工維護各種亂七八糟的物件還是很麻煩。下一章我將介紹 LaravelContainer 的實現方式。

作者下一章原文中講的是 ContainerSymfony 2 中的實現,我會把它換成 Laravel

本作品採用《CC 協議》,轉載必須註明作者和本文連結
原創。 所有 Laravel 文章均已收錄至 Github laravel-tips 專案。

相關文章