現代的なPHPのフレームワークではほぼ導入されている依存性の注入について、なるべく基本的なところから書いていこうと思います。
忙しい人向けのまとめ
・依存性の注入とは、あるクラスのインスタンスを別のクラスに渡すこと
・インスタンスの作成、注入と、インスタンスの利用を別のクラスで行うことで関心の分離ができる
・インターフェースを使うことで、疎結合になる
依存性の注入とは
クラスのプロパティに別のクラスのインスタンスをもっていたり、あるメソッドの引数に別のクラスのインスタンスを渡すようなパターンを、依存性の注入といいます。
さて、大事なのは、そのようなパターンを使用するとなにがうれしいのかということですね。
ですが、その前に「疎結合」と、「関心の分離」について理解していることが重要ですので、
以降の章では、ソースコードをもとにそれらの概念について説明したいと思います。
疎結合
モジュールに互換性を持たせるためには重要な考え方として、
疎結合であるというものがあります。
あるクラスのメソッドの中で、別のクラスのインスタンスを生成したい場合はあるかとおもいますが、そのような実装は結合度が高いものとなってしまいます。
例えば、GoogleのAPIと通信してユーザ一覧を取得したい場合、
class GoogleCommunication
{
construct()
{
$this->ch = curl_init();
}
public function getGoogleUsers()
{
curl_setopt($this->ch, CURLOPT_URL, CONST::GOOGLE_USER_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
return curl_exec($this->ch);
}
}
class User
{
construct()
{
$this->googleCommunication = new GoogleCommunication();
}
public function getList($url)
{
$result = $this->googleCommunication->getGoogleUsers();
return json_decode($result);
}
}
$user = new User();
$list = $user->getList();
Userクラスのなかで、GoogleCommunicationクラスのインスタンスが生成されているため、例えば、GoogleのAPIがXMLを返すようになったり、GoogleではなくMicorsoftのApiを使いたい、となったときなどは、Userクラスへの修正が必要になってきます。
そこで、次のように実装してみます。
interface BaseCommunication
{
public function request();
public function setOpt();
public function parseResponse();
}
class GoogleCommunication implements BaseCommunication
{
construct()
{
$this->ch = curl_init();
}
public function request() {
$this->setOpt();
$this->response = curl_exec($this->ch);
return $this->parseResponse();
}
protected function setOpt() {
curl_setopt($this->ch, CURLOPT_URL, CONST::GOOGLE_USER_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
}
protected function parseResponse() {
return json_decode($this->response);
}
}
class User {
construct(BaseCommunication $communication) {
$this->communication = $communication;
}
public function getList() {
$result = $this->communication->request();
return $result;
}
}
$communication = new GoogleCommunication();
$user = new User($communication);
$list = $user->getList();
こんどはUserクラスの中でGoogleCommunicationクラスの生成が行われていません。
また、Userクラスの引数として、インターフェイスが指定されていることにも注目してください。
$communicationプロパティは、BaseCommunicationインターフェイスを実装したクラスであること、という制限はありますが、特定のクラスでなければならないということはありません。
そのため、たとえば、MicrosoftのAPIに変更したい、となったときも、次のようにすればいいのです。
// マイクロソフトAPIとの通信
$communication = new MicrosoftCommunication();
$user = new User($communication);
$list = $user->getList();
Userクラスを修正せず、$communicationプロパティの中身を変更できました。
このようにクラス同士の依存度が低い状態を疎結合であるといいます。
関心の分離
関心の分離とはひとつのクラス、またはメソッドの中では一つの機能を実装するべき、という考え方です。
テストのしやすさ、修正のしやすさを保つためには重要な考え方です。
次のメソッドをみてください。
class User
{
function getAuthorUserList()
{
$dbh = new PDO(~接続情報~);
$sql = 'SELECT id,name FROM USERS WHERE type=:type';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array('type' => 'author'));
$list = $sth->fetchAll();
$userNameList = array_column($list, 'name', 'id');
return $userNameList;
}
}
getAuthorUserListメソッドの中では、データベースへの接続と、データの取得、取得したデータの成形が行われています。
つまり、三つの機能を一つのメソッドで実装しているということになります。
これでは、再利用もしにくいですし、修正したいとなった場合は、
全ての処理をみて影響がでないか確認しなければいません。
では、次のようにするとどうでしょう?
class UserDao
{
private $table = 'USERS';
constructor()
{
$this->dbh = new PDO(~接続情報~);
}
public function fetchUsersByType($type)
{
$sql = 'SELECT id,name FROM ' . $this->table . ' WHERE type=:type';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(['type' => $type]);
$list = $sth->fetchAll();
return $list;
}
}
class User
{
public function getUserList()
{
$userDao = new UserDao();
$list = $userDao->fetchUserByType('autor');
return $this->getNamesById($list);
}
private function getNamesById($list)
{
return array_column($list, 'name', 'id');
}
}
DBへのアクセスはUserDaoクラスが行い、データの整形はUserクラスが行う設計にしました。
そのため、他のクラスでUsersテーブルのデータが欲しいときは、UserDaoの再利用もできますし、個別のメソッドに対して修正するときは、そのメソッドの中だけを確認すれば他のメソッドへの影響はあまり考えなくてもいいです(このような設計を、副作用がない、といいます。副作用があるメソッドに対して修正する場合は、他のメソッドへの影響も考える必要があります。)
そして、修正箇所を探すのも簡単です。
実装は大変になったと思われるかもしれませんが、ある程度以上の規模のプログラミングにとっては修正のしやすさが一番大事です。
依存性の注入
実は、依存性の注入自体は、とてもシンプルなもので、次のパターンはすべて依存性の注入です。
- あるクラスのコンストラクタの引数に、別のクラスのインスタンスを渡す。
- あるクラスのメソッドの引数に、別のクラスのインスタンスを渡す。
例として、次の実装を見てください。
interface BaseCommunication
{
public function request();
public function setOpt();
public function parseResponse();
}
class GoogleCommunication implements BaseCommunication
{
construct()
{
$this->ch = curl_init();
}
public function request() {
$this->setOpt();
$this->response = curl_exec($this->ch);
return $this->parseResponse();
}
protected function setOpt() {
curl_setopt($this->ch, CURLOPT_URL, CONST::GOOGLE_USER_URL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
}
protected function parseResponse() {
return json_decode($this->response);
}
}
class User {
construct(BaseCommunication $communication) {
$this->communication = $communication;
}
public function getList() {
$result = $this->communication->request();
return $result;
}
}
$communication = new GoogleCommunication();
$user = new User($communication);
$list = $user->getList();
このコードは、疎結合の説明の際出てきたものです。
じつは、このコードではすでに依存性の注入が行われています。
$communication = new GoogleCommunication();
$user = new User($communication);
ここでは、コンストラクタを利用した依存性の注入が行われています。
多くのPHPフレームワークでは、このようなインスタンスの生成を、コンテナというクラスで行っています。
インスタンスの生成とインスタンスの利用という2つの機能を別々のクラスで行うことで、関心の分離ができているわけです。
また、インスタンスを利用する側のクラスでは、インターフェイスを利用するような実装にすることで、疎結合なプログラムも実現できます。
あとがき
参考記事のほうでは、サービスロケータとの違いについても解説されていますので、興味がありましたらご覧ください。