名前空間付き函数 VS 静的メソッド ファイッ

  • 18
    Like
  • 1
    Comment

名前空間を活用した【PHP超入門】名前空間(namespace・use)についてが網羅的で良かったので、ふだんから考へてることをまとめたポエムです。結論はないです。

はじめに

オブジェクト指向的な設計の観点からはユーティリティクラス(静的メソッドのみを持つクラス)は、均衡を崩すものとして忌み嫌はれる存在であることは意識しておいた方が良いです。関数とユーティリティクラスは禁止 - 株式会社アークシステム(infoARK)のような議論についてはしっかりと咀嚼する価値があります。

…………が、そんな高尚な設計の話をしたり設計の批判をするのは簡単ですが現実世界では動くコードが正義です。オブジェクト指向の大先生にならなきゃプログラムを書いちゃいけないなんてことは断じてありません。あなたの目的を達成するためにプログラミング言語は存在するのだから、利用できる機能はなんでも利用しませう。

そんなわけで、この記事では大手を振って函数とユーティリティクラスの礼賛をします。それと記事の題名について最初に個人的な見解を伸べると、私は静的メソッドを利用することの方が比較的多いです。

名前空間について

はじめにPHPマニュアルの名前空間をひととほり読むことをおすすめします。読みましたね?

函数呼び出しっぽいものの分類

函数とかメソッドとかいろいろあるので、呼び出しの書式ごとに分類を整理します。一般的にはクラスに属しないものを「函数」、クラスに属するものを「メソッド」と呼びます。でもPHPで定義するための文法上のキーワードはメソッドふくめてぜんぶfunctionです。 へんなの。

コード 説明
hoge() 自身と同じ名前空間(なければトップレベル名前空間)hoge函数
\fuga() トップレベル名前空間のfuga函数
foo\bar\buz() foo\bar名前空間に属するbuz函数
$obj->cool() インスタンスオブジェクト$objcoolメソッド
foo\Bar::hot foo名前空間に属するBarクラスのhot静的メソッド
$piyo() callableなローカル変数 $piyo の呼び出し

callableな値$piyoの実態は無名関数だったり、配列だったり、文字列だったりします。詳しくは可変関数を呼んでください。

$piyo = 'hoge'; // hoge() と同じ
$piyo = '\fuga'; // \fuga() と同じ
$piyo = 'foo\bar\buz' // foo\bar\buz() と同じ
$piyo = [$obj, "cool"]; // $obj->cool() と同じ
$piyo = ['foo\Bar', 'hot']; // foo\Bar::hot() と同じ
$piyo = function () use ($v) { ... }; //無名函数(クロージャ)

名前空間付き函数について

【PHP超入門】名前空間(namespace・use)についてに概ね網羅されてますので、よくお読みください。

クラスの静的メソッド

静的メソッドとは、インスタンス化($obj = new HogeClass)しなくても利用できるメソッドのことです。

final class HogeUtil
{
    /**
     * "foo"を$n回繰り返した文字列を返す
     *
     * @param  int    $n
     * @return string
     */
    public static function foo($n)
    {
        return str_repeat("foo", $n);
    }
}

一切インスタンス化させずに静的メソッドを利用するために用意するクラスのことを「ユーティリティクラス」と呼びます。

オートローディング

ところで、PHPにはクラスのオートローディングと呼ばれる仕組みがあります。モダンなPHPでは、いちいちファイルごとにinclude_onceとかrequire_onceと書かずに読み込む方法がベストプラクティスとされてます。

モダンPHPアンチパターン #ファイルごとにrequire/includeを書くとかにちょろっと書いたんですが、基本はComposerをちゃんと設定すれば、それだけでクラスを自動ロード化することができます。 (ちょっと小声でステマをさせていただくと、WEB+DB PRESS Vol.91に名前空間とクラスについてしっかりと説明した記事を書いたので、よろしければ買って読んでね!)

オートローディングの限界

さて、クラスは自動ロードできるのですが、名前空間に直接定義された函数はできません。事前に全て読み込んでおくか、必要に応じてinclude_onceまたはrequire_onceさせるしかありません。

名前空間付き函数をComposerのライブラリとして提供するには、composer.json的には{"autoload": {"files": ["src/functions.php"]}}のような定義で事前に読み込んでおくことになります。

ちなみに私がふだん仕事で携ってるプロジェクトの共通ロジック部分には1286クラス、7421メソッドありました。こんなの全部を事前ロードはあんまりやりたくない。

grepの敗北

useを利用すると長い名前空間・クラス名・函数名を省略することが簡単にでき、とてもべんりです… 便利ですが、たとへば函数の名前を変更しようと思ったとき、あるいは引数の仕様を変更しようと思ったときにuseを使ってると少々厄介なことになります。

人間、数多くのコードを書いたり息を吸ったり吐いたりしてると、この「メソッドいまいちだったな」なんて気分になることは割とよくあります。名前空間なしのクラスの静的メソッドHoge::fuga()のメソッドをリネームするのは簡単です。grepなどのテキスト検索ツールでgrep -r Hoge::fuga( .なんて検索してやれば利用箇所を洗い出して置換していくことができます。極端な話、sedでリファクタリングができます。

世のなかにはPHP-Parserのようなライブラリも多くあるので、こういったものをうまく組み合せればもっとうまくリファクタリングができるはずなんですが…

静的メソッドの良くないところと対処

バッドノウハウです。わかってやってます(断言)

  • 継承したクラスから呼び出すことができる
    • 継承されると、単純にgrepできない
    • ユーティリティクラスをfinal class Hogeのように定義すれば継承されないことを保証できる
  • インスタンス化して呼び出すことができる
    • 静的メソッドとして定義しても$obj->fuga()として呼び出すことが可能
    • コンストラクタをprivate function __construct()で定義してインスタンス化を封じる

traitってどうなの?

トレイト(trait)はPHP 5.4で導入された仕組みで、継承とは別の軸でクラスに機能を組込むことが可能です。拙作のBaguettePHP/objectsystemtraitを活用してオブジェクトを機能的に拡張することができます1

さて、実はtraitに定義した静的メソッドは継承しなくても呼び出すことができます。またクラスにuseして利用できるようになるので、RubyのMathモジュールをincludeするような使用感2になって良いのではないか、と期待したのですが……useしちゃったらgrepがめんどくさくなる問題は何も解決しないので私はやめました。

traitはうまく利用すれば効果的で素晴しい機能なのですが、下手な作りにすると非常にコードが追ひにくくなって「ユーティリティクラスにした方がはるかに読みやすい」事態にも容易になりえるので、現実とはままならないものです。


特に落ちはなく結び

世の中のPHPerはどうやってリファクタリングを断行してるんだろう… ちゃんと静的解析できればリネームに限らずスマートにリファクタリングできるはずなんだけど、手元のツールできちんとそれが実現できそうなのはPhpStormしかなかった。

水は低きに流れる。みんなgrepが好きなので静的メソッドに流れる。あんまり健康な状態じゃないのでテクノロジーで反抗して勝利していきたい。

裏技

AspectMockと名前空間を悪用活用すると、PHPの標準函数をモック化できるぞ! たぶん!

脚注


  1. private/protectedなプロパティを外部から読み込み可能にする - Qiitaとかインスパイヤされて掲示板を作りたくなった(3) - Qiitaあたりで利用方法を解説してます ヾ(〃><)ノ゙ 

  2. ちなみにRubyのMathメソッドのような仕様の