PHPの名前空間の効果的な使い方を考える

Posted by Hiraku on 2014-01-27

PHPは5.3から名前空間が導入され、名前の衝突を避けるため長いクラス名をつけることから解放されました。しかしながら、名前空間を使ったコードは、名前空間を使っていないものに比べて本当に読みやすくなっているのでしょうか?

ここで例を挙げます。PHPの良質なソースコードと言えば、私はZend Framework(ZF)やSymfonyを思い浮かべるのですが、ZFのとあるクラスの冒頭を見てみましょう。

https://github.com/zendframework/zf2/blob/master/library/Zend/Mvc/View/Console/RouteNotFoundStrategy.php
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\Mvc\View\Console;

use Zend\Console\Adapter\AdapterInterface as ConsoleAdapter;
use Zend\Console\ColorInterface;
use Zend\Console\Response as ConsoleResponse;
use Zend\Console\Request as ConsoleRequest;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\ModuleManager\ModuleManagerInterface;
use Zend\ModuleManager\Feature\ConsoleBannerProviderInterface;
use Zend\ModuleManager\Feature\ConsoleUsageProviderInterface;
use Zend\Mvc\Application;
use Zend\Mvc\Exception\RuntimeException;
use Zend\Mvc\MvcEvent;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\ServiceManager;
use Zend\Stdlib\ResponseInterface as Response;
use Zend\Stdlib\StringUtils;
use Zend\Text\Table;
use Zend\Version\Version;
use Zend\View\Model\ConsoleModel;

class RouteNotFoundStrategy extends AbstractListenerAggregate
{
//...

use多すぎでしょう。。
よくよく読んでいると、単にuseしているだけではなく、別名を付けたりしています。

個人的な意見ですが、use文が多いソースコードは読みにくいと思っています。何故ならば、ソースコードのある部分だけ抜き出したとき、use文を参照しないと本当のクラス名がわからないからです。

//長大なソースコードが続いた後
//
  function hoge(Fuga $fuga) {
    $fuga->setOption(new Moo(Moo::OPT1));

    //FugaとMooというクラス名は、実名ではないかもしれない!
  }

ソースコードを上からすらすらと読み解くには、use文が100行あれば、その100行を暗記しないといけないのです。暗記できないなら、「このクラス名って実体どこだったかな…」となるたびにuse文の定義に戻って確認して、を繰り返すことになります。なので読みにくいのです。

そういえば、オートローダーが使われるようになる以前、太古の昔は、クラス定義の前に必死でrequire_once ...を書いていましたが、今はrequire_onceの代わりにuse文が並ぶようになっています。

せっかくrequire_onceが撲滅できたのに、何ということだ!

今のところ名前空間の使い方については、VendorPrefix以外のことはPSRにも明記されておらず、みんな空気を読んで使っているようです。

この記事では、どうすればuse文を減らし、読みやすさと簡潔さを両立できるか、名前空間の使い方を考えてみたいと思います。

useを書かないことも検討する

PHPでは別にuseしなくても、完全修飾名でクラスを書けばいつでも任意のクラスを使うことができます。全てのクラスがpublicだからですね。

例えば、そのクラスがソースコード中で一回しか登場しないのであれば、use文を書かずに、完全修飾名で直に書いてしまった方が、短くシンプルになります。

例えばこれ。PHPUnitのテストケースとしてよく見る例ですが、よくよく見るとuse文消したほうがシンプルですよね。

<?php
namespace My\Tests;

use PHPUnit_Framework_TestCase as TestBase;

class HogeTest extends TestBase
{
}

これで十分。

<?php
namespace My\Tests;

class HogeTest extends \PHPUnit_Framework_TestCase
{
}

しかし、何度も登場するのであれば、やはり一度useして別名を付けておいた方がすっきり書けるでしょう。問題はuseの書き方です。

クラスをuseするのではなく名前空間をuseする

use文でインポートできるのは、クラス名、インターフェース名、トレイト名、そして名前空間名です。関数名や定数名は5.6でインポートできるようになる予定です。

そう、名前空間名もuseできます。別にクラス名を直接インポートしなくても、適当な名前空間で止めておけばよいのです。

つまり、↓のように書かずに、

<?php
use Acme\Stdlib\ModuleInterface;
use Acme\Stdlib\Text;

function hoge(ModuleInterface $module) {
  $t = new Text;
  //...
}

あえて↓のように書くのはどうでしょうか?

<?php
use Acme\Stdlib;

function hoge(Stdlib\ModuleInterface $module) {
  $t = new Stdlib\Text;
  //...
}

ソースコード本体に書くクラス名は長くなりますが、私はこの方が読みやすいと感じます。

もし名前空間だけuseするスタイルに変えれば、冒頭のZFのコードはこんな感じになります。

<?php
namespace Zend\Mvc\View\Console;

use Zend\Console;
use Zend\EventManager;
use Zend\ModuleManager;
use Zend\Mvc;
use Zend\ServiceManager;
use Zend\Stdlib;
use Zend\Text\Table;
use Zend\Version\Version;
use Zend\View\Model\ConsoleModel;

class RouteNotFoundStrategy extends EventManager\AbstractListenerAggregate
{
//...

クラス一個だけuseしているケースはそのままにしましたが、use文が20行から9行に減りました!

use Zend\Console\Response as ConsoleResponse;
use Zend\Console\Request as ConsoleRequest;

だったところとか、use Zend\Console;で止めておけば、Console\ResponseConsole\Request と書けて、むしろ自然に見えませんか?

同時に使うクラスは同じ名前空間に配置する

名前空間をuseするようにしたからといって、例えば以下のように「使うクラスが全部違う名前空間に所属している」場合は全然記述量が減りません。


<?php
use Acme\A\Aaa;
use Acme\B\Bbb;
use Acme\C\Ccc;
//...

ここでは「同時に使うクラスを、別々の名前空間に配置するのは設計が悪い」と仮定してみましょうか。

同時に使うクラスを同じ名前空間に配置してあれば、use文は最小限書けば十分なはずです。

<?php
use Acme\Modules;

$a = new Modules\Aaa;
$b = new Modules\Bbb;
$c = new Modules\Ccc;

PHPの名前空間はパッケージである

すなわち、より自然な使い方を追求すれば、PHPの名前空間とは、同時に使用することの多いクラスをまとめたものということになります。

これって、パッケージとかモジュールとか呼ばれるモノに相当すると思います。PHPの言語仕様上は存在しない言葉ですが、当てはめて考えるとわかりやすくなります。

PSR-0の定めるVendorPrefixを守ると、名前空間を利用したクラスの命名規則は以下のような3階層が基本形になるのではないでしょうか。


VendorPrefix \ PackageName \ classname

このパッケージを利用する際はuse VendorPrefix\PackageName;として、new PakcageName\classnameのように利用します。

サブ名前空間は内部パッケージのために使う

ここまでだと、3階層あれば名前空間は事足りるということになりますが、PHPの仕様上はもっと深い名前空間を付けることができます。この深い階層は何かの役に立たないのでしょうか?

名前が長く、深い階層であるほどクラスはタイプ数が増えて使いにくくなります。つまり他のパッケージから使われる、パブリックなクラスほど浅い階層に配置し、パッケージの内部でしか使わない、内部クラス(PHPには概念上存在しませんが)であるほど深い階層に配置するべきです。

たとえば、A\B\PublicClassというクラスがあり、この内部でしか使わないXXClassYYClassという2つのクラスがあるとしましょう。XX、YYはどの名前空間に配置するべきでしょうか?

A\B\XXClass, A\B\YYClassとして配置することもできますが、これだとPublicClassと同じ名前空間に属してしまうため、「PublicClassと同時にXXClassやYYClassも使うことが多い」という風に見えてしまいます。

そこで、より深い階層にXXとYYを配置して、あえて使いにくくしてみます。いくつかやり方があると思いますが…

■クラス名と名前空間名を重ねる方法

A\B\PublicClass
A\B\PublicClass\XXClass
A\B\PublicClass\YYClass

安直な命名ですが、こんな風にすると名前空間「A\B」の直下にはPublicClassしか存在しない状態にできます。


<?php
namespace A\B\PublicClass;

class XXClass {}
class YYClass {}
<?php
namespace A\B;

class PublicClass {
  //...
  function doSomething() {
    $x = new PublicClass\XXClass;
    $y = new PublicClass\YYClass;
  }
  //...
}

■内部用の名前空間を定義する方法

一つのクラスだけではなくパッケージ全体で使うけれど、パッケージの外に公開する必要がないような場合もあるかもしれません。 その場合、特定のクラスの下にあるような配置は違和感がありますね。Internalといった内部用の名前空間を作るとそれっぽい雰囲気になります。

<?php
namespace A\B\Internal;

class XXClass {}
class YYClass {}
<?php
namespace A\B;

class PublicClass {
    function doSomething() {
        $x = new Internal\XXClass;
        $y = new Internal\YYClass;
    }
}

Internalとしましたが、内部クラスの役目を考えて、具体的なパッケージ名を付けてもいいでしょう。

面白いことに、内部パッケージという別のパッケージを使っているにもかかわらず、「use A\B\Internal;」を書かなくても使えます。名前空間が一階層だけずれている状態だからですね。

内部パッケージの構成要素として、更に内部パッケージを作ることもできます。

こんな感じで、内部の内部の更に内部のパッケージ…というように前空間の階層構造と、パッケージの階層構造を対応付けることができそうです。

アンチパターン:継承と名前空間

ここまでの考察から、「同時に使うクラスを同じ名前空間に配置するべきで、それ以外の理由で名前空間を決めてはならない」というような原則がわかります。

例えば、クラスの継承関係を基にした配置は悪手です。こんなクラスを見たことはないでしょうか?

<?php
namespace A {
  class B {}
}

namespace A\B {
  class C exitends \A\B {}
}

namespace A\B\C {
  class D extends \A\B\C {}
}

親クラスを継承するたびに、一階層名前空間が深くなるような規則を使っています。あまり深く考えなくていいので、私も昔よくこんな書き方をしていました。

たいていの場合、具象クラスの方が抽象クラスより使う機会が多いのではないでしょうか。上記の規則だと実際に利用する具象クラスは一番深い階層になってしまい、使いにくい長ったらしい名前になってしまいます。

継承関係と名前空間は対応している必要はないのです。同じ名前空間上に親子関係のクラスを並べてもいいですし、親クラスの方が子クラスより深い階層にあってもおかしくありません。 むしろ、親クラスであるほど深い階層に配置し、子クラスであるほど浅い階層に配置する傾向になるかもしれません。

大事なのは、「クラスが実際にどう使われるか?」を考えた上で配置することだと思います。

パッケージ設計の原則を適用する

アンクルボブことRobert C Martin が「アジャイルソフトウェア開発の奥義」において、パッケージ設計の原則について述べています。名前空間はパッケージなのであれば、同じ原則を適用すると使いやすく柔軟でかつ堅牢な設計が実現できるはずです。 私も理解しきってないので、ここではメモに留めます。

REP: Reuse-Release Equivalency Principle(再利用・リリース等価の原則)
再利用の単位とリリースの単位は等価になる。
CRP: Common Reuse Principle (全再利用の原則)
パッケージに含まれるクラスは、すべて一緒に再利用される。つまり、パッケージに含まれるいずれかのクラスを再利用するということは、その他のクラスもすべて再利用することを意味する。
CCP: Common Closure Prinsiple (閉鎖性共通の原則)
パッケージに含まれるクラスは、みな同じ種類の変更に対して閉じているべきである。パッケージに影響する変更はパッケージ内のすべてのクラスに影響を及ぼすが、他のパッケージには影響しない。
ADP: Acyclic Dependencies Principle (非循環依存関係の原則)
パッケージ依存グラフに循環を持ち込んではならない。
SDP: Stable Dependencies Principle(安定依存の原則)
安定する方向に依存せよ。
SAP: Stable Abstractions Principle(安定度・抽象度等価の原則)
パッケージの抽象度と安定度は同程度でなければならない。

クラスの設計原則であるSOLIDの5大原則の方は有名ですが、パッケージの設計原則まではなかなか頭が回らないです。。 精進せねば。

まとめ

私の主張はこの一点です。

  • use文で一個一個クラスをインポートしているソースコードは読みにくい。何とかしてほしい。

それを踏まえて、use文を減らすアイデアをまとめました。

  • PHPの名前空間はパッケージに相当する概念である
  • 同時に使うクラス、インターフェースなどを同じ名前空間に配置するべきである
  • 逆に言えば、同時に使わないクラスは同じ名前空間に配置してはならない
  • あるパッケージの内部のみで使用するクラスは一段階深い名前空間階層に配置し、内部パッケージとする
  • パッケージ設計の原則を当てはめるとよい設計になりそう(自信ない)

以上は私の勝手なアイデアで、特に大きな実績もないです。しかしPHPの名前空間についての命名規則とか他にガイドライン的なのがあるのかと言うと特に見つからず、まあ困っているのです。

ちょっと今回の記事だと実例に乏しいので、次回で何か実例を出せるようにします。

※ちなみに、Javaのパッケージ命名ルールよりもC#の名前空間規則の方がPHPに似ていて、参考になるように思います。

名前空間の名前

keyword: 設計

PHPの最新記事

×

この広告は180日以上新しい記事の投稿がないブログに表示されております。