みんな意外と抽象化している
実際、あらためて抽象化と言われるとよくわからない。でも意外に身近なところで抽象化をしています。
たとえば、MySQLってなに?ってエンジニア以外の人に聞かれた時、皆さんならなんと答えるでしょうか。
「データを貯めておいて、それぞれをくっつけたり集計したりしながら取り出せるやつ」
とかでしょうか。実はこれ、立派な抽象化です。
我々エンジニアでも、MySQLのオプティマイザがクエリーを実際どのように解析して、実行計画をたてているかまで把握している人は少ないでしょう(僕もしりません)。
SQLは知っているし、EXPLAINで実行計画を調べることもできるけど、実際クエリーを発行してから結果を得るまでになにが起こっているかはふわっとしか理解していない。これ、抽象化して理解しているって事になると思います。
言語化するなら、
「大事なとこだけ押さえて、あとはふわっとしてる」 = 抽象化
って感じでしょうか。
システムにおける抽象化
「大事なとこだけ押さえて、あとはふわっとしてる」
設計をシステムにどうやって組み込むのでしょう。我々は細かいところまで仕様を定義し、そのとおりに動くように日々実装しているのに、よりにもよってふわっとしたままにしているってのがどうも腑に落ちないと思います。
実際そのとおりで、プログラムはふわっと記述できません。
「大事なのはここだけ!あとはよしなにしといて!」と書ける言語はまだないでしょう。
抽象化するポイントは、主に自分の下位のレイヤーです。
下位のレイヤー
DDDのレイヤーアーキテクチャのことではなく、一般的な「層」という理解で構いません。たとえば、MVCフレームワークも「層」で処理が分かれています。
一般的なMVCにビジネスロジックを記述するService層を追加したケースを表してみました。この場合、Serviceレイヤーは下位のModelレイヤーのことを抽象化できるのです。ただし、疎結合になっていればという注釈が付きます。
疎結合
密結合、疎結合という言葉を耳にしたことがあると思います。これもなんか分かりづらい言葉ですが、
くっつき度合い
と思っていただければ間違ってないと思います。
寝るときも食事する時も離れずにくっついていて、
「あなたがいないと私だめなの」「僕もだよ」
と言っている付き合いたてのカップルは、密結合。
お互い自分の時間を大切にし、
「あなたといるのも楽しいけど、友達と趣味をやるもの好きだわ」「僕もだよ」
というカップルは疎結合と言えます。
これは、いいかえれば、依存度とも言えると思います。
下位レイヤーのModelに対し、**「君じゃなきゃだめなんだ」と言っているプログラムは密結合で、「君とも楽しいけど別のModelとも楽しい」**と書いてあるプログラムは疎結合です。
依存の高いプログラム
ずばり、こう書いてあると、依存しているプログラムです。このようにコーディングされていると、抽象化はできません。
public function detail(int $id): array
{
$model = new CustomerModel();
return $model->getById($id);
}
このdetailメソッドは、CustomerModelがいないと動きません。**「あなたがいないと私だめなの」**状態。密結合なのです。
これを疎結合に記述できれば、Modelを抽象化できるということになります。
Dependency Injection
DIの説明だけで別記事を投稿できるくらいなので、本記事ではかいつまんで説明します。
ずばり、インスタンスを外部から入れると思っていただければだいたいあってるかと思います。先ほどのプログラムの例ですと、CustomerModelをnewしたインスタンスを、外部から入れる、ということになります。
class CustomerService
{
/** @var CustomerModel */
private $model;
public function __construct(CustomerModel $customer)
{
$this->model = $customer;
}
public function detail(int $id): array
{
return $this->model->getById($id);
}
}
コンストラクタでCustomerModelのインスタンスを受け取り、privateプロパティに代入しています。
Laravelでこのように記述をすると、このServiceクラスをインスタンスにする際、自動でCustomerModelをnewし、コンストラクタに代入してくれます。
Laravelが搭載している、強力なDIコンテナ機能で依存性を解決してくれるのです。このDIコンテナ機能こそ、Laravelが人気の理由といっても過言ではないと思います。
こういう記述にすれば、Serviceをインスタンスにする際に、中で使うModelを変更できるようになったことがわかると思います。**「君とも楽しいけど別のModelとも楽しい」**を実際に行える、疎結合なプログラムになりました!!
...と、言いたいいところですが、これだけでは不十分なのです。
そうです。CustomerModelとタイプヒントしている以上、実質、CustomerModelしか代入できないのです。テストコードを記述する際にCustomerModelのモックを代入するような事はできますが、完全な抽象化とまではいってません。
大事なとこだけおさえて、あとはふわっと
抽象化って大事なとこだけおさえて、あとはふわっと
ってことだと説明しました。
class CustomerService
{
/** @var CustomerModel */
private $model;
public function __construct(CustomerModel $customer)
{
$this->model = $customer;
}
public function detail(int $id): array
{
return $this->model->getById($id);
}
}
では、このdetailメソッドにおいて、大事な事はなんでしょうか。
それは、下位レイヤーのインスタンスにgetByIdメソッドが存在することにほかなりません。正味な話、getByIdメソッドの中で何をやってるかはふわっとしてても、IN/OUTが定義され、メソッドが存在しているだけでいいのです。
コンストラクタでうけとるインスタンスに、getByIdメソッドが存在していれば動いてしまいます。
大事なところは束縛する
動きも仕組みもまぁ、どうでもよい。メソッドが確実にあって、引数と戻り値が確定していてば、成立する。すなわち、抽象化できた状態にするには、大事なところを束縛します。
PHPにおける束縛はinterfaceと覚えてください。interfaceは契約と捉えられる事が多いですが、束縛のほうがイメージ付きやすいです。
**「毎晩電話くれる彼じゃなきゃやだ」**という束縛はこんなふうになるでしょう。
interface MyGuyInterface
{
public function callTelephoneEveryNight(int $telephoneNo);
}
この束縛を持っている彼でなければ、そもそもインスタンスにすらできません。
// Taroは毎晩電話する機能をもっているので、new(付き合うことが)できます
class Taro implements MyGuyInterface
{
public function callTelephoneEveryNight(int $telephoneNo)
{
//
}
}
// Samはイケメンなので都合のいいときしか電話してきません。エラーです
class Sam implements MyGuyInterface
{
public function callTelephoneSpot(int $telephoneNo)
{
//
}
}
先程までの例に当てはめると
interface ModelInterface
{
public function getById(int $id): array;
}
で束縛し、それをimplementsしていれば、CustomerModelでなくても使えるということになります。
DIコンテナの本領発揮
コンストラクタで、Interfaceを注入します。
class CustomerService
{
/** @var CustomerModel */
private $model;
public function __construct(ModelInterface $model)
{
$this->model = $model;
}
public function detail(int $id): array
{
return $this->model->getById($id);
}
}
Interfaceは束縛なので、getByIdが必ずあることは確約されました。しかし、ただの束縛であって、実際に動くプログラムコードはないです。このままでは当然エラーになります。そこで、LaravelのDIコンテナが活躍します。Interfaceと実装を結合させて、インスタンスを注入することができるのです。
$this->app->bind(ModelInterface, CustomerModel);
たった1行の記述で、ModelInterfaceがコンストラクタに書かれていたら、CustomerModelをnewして代入してくれます。この記述は、サービスプロバイダに記述するので、ビジネスロジックやモデルに手を加える必要がありません。
今日からMySQLやめてdynamoDBにします
まずありえないですが、リリース1ヶ月前にこうやって言われたとしましょう。
CustomerModelは、MySQL用のモデルで、SQLが記述してあります。それを、CustomerDynamoModelに変更しなければならない。
こんなときこそ、抽象化にしておいてよかった!という場面なのかもです。
抽象化=残業なしで帰れる
メソッドをInterfaceに切り出して、それを注入し、依存解決をさせて実装をすすめてさえいれば、急にCustomerModelから、CustomerDynamoModelに変更しろといわれてもいいんです。
CustomerDynamoModelにInterfaceがimplementsさえしていれば、あとはDIコンテナで結合先を変更するだけでPHPUnitもパスするでしょう。はい、簡単に切り替えることができました。
もしこれが密結合になっていて、あちこちでCustomerModelをnewしていたとしたら。テストを含めてどれだけの工数がかかるか…想像するだけで泣きたくなります。
みなさんもレイヤーを抽象化し、DIをすることで残業なしのエンジニアライフを送りましょう!!