本稿ではPHPの静的メソッドが何のためにあるかを考えるものである。
クラスとインスタンスの概念的な関係性
静的メソッドを理解するには、クラスとインスタンスの関係性を理解していなければならない。
クラスとインスタンスの関係は何だろうか?
一言で言えば、クラスは、複数のインスタンスの集合である。概念的な集合であって、インスタンスの配列という意味ではない。例えば、$user1のオブジェクトと$user2のオブジェクトを総じて、Userと呼べるのは、Userが$user1と$user2の集合だからだ。
User = { $user1, $user2 }
以上の集合論的な観点を踏まえると、静的メソッドは何のためにあるかといえば、集合に含まれるすべてのオブジェクトに共通した処理(公理ともいう)を記述するためにあると言える。
逆を言えば、集合の一要素にすぎない$user1にのみ関係する処理は静的メソッドにはならない。Userクラス(ここまで読んだみなさんなら、もうこの一言で$user1と$user2と...$userNすべてのことを指していることは上の説明でお気づきだろう)に関係する処理はUserクラスの静的メソッドになりうる。
もしも静的メソッドが無かったら
例として、3つの商品の金額の合計を求めるケースを考えて見よう。
もしも、静的メソッドがない言語だったら、合計金額を求めるメソッドは、商品インスタンスのメソッドとして定義することになるかもしれない。
class Product
{
// ...コンストラクタなど略...
public function calculateTotalPrice(
Product $otherProduct1,
Product $otherProduct2
): int {
return $this->price + $otherProduct1->price + $otherProduct2->price;
}
}
このcalculateTotalPriceメソッドを使おうとするとこうなる:
$product1 = new Product(100); // 100は100円ということ
$product2 = new Product(100);
$product3 = new Product(100);
$totalPrice = $product1->calculateTotalPrice($product2, $product3);
ここでは$product1が主軸となって合計金額が計算されたが、主軸となるのは$product2でもいいはずだ。3つの商品を対等に扱えていないため、calculateTotalPriceメソッドの置き場としてはあまり心地の良いものとは言い難い。これは1つ目の問題だ。
では、$totalPriceCalculatorというオブジェクトを作って、そいつに計算させてはどうか?
class TotalPriceCalculator
{
public function calculateTotalPrice(
Product $product1,
Product $product2,
Product $product3
): int {
return $product1->price + $product2->price + $product3->price;
}
}
$totalPriceCalculator = new TotalPriceCalculator();
$totalPrice = $totalPriceCalculator->calculateTotalPrice(
$product1,
$product2,
$product3
);
確かにこうすれば、どの商品も主軸になっていおらず、対等に扱われているため、最初の実装のような気持ち悪さはない。さっきよりは良い実装になったと思う。
しかし、今度は、TotalPriceCalculatorがProductを知りすぎてしまっている。Productのprice属性が何なのか知っているがために、例えばprice属性がintからCurrencyクラスに変更されたとしたら、TotalPriceCalculatorの実装も変更を余儀なくされる。つまり、別クラスが知りすぎてしまうと変更が面倒な構造になるだけでなく、変更対応の漏れが発生しやすく、バグを呼び寄せやすい構造になってしまう。オブジェクト指向では大事なカプセル化を毀損しているとも言える。これは2つ目の問題だ。
静的メソッド
ここで一旦出てきた問題を整理しよう。
静的メソッドが使えないとなると2つの問題が引き起こされる:
- 複数のインスタンスにまたがった処理の置き場として、インスタンスメソッドは常にいい場所ではない。
- かといって、意味的に関係していないオブジェクトにその処理を置くとカプセル化が毀損される。
以上の問題を解決するには、Productの静的メソッドとしてcalculateTotalPriceを生やすといい。
class Product
{
// ...コンストラクタなど略...
public static function calculateTotalPrice(
Product $product1,
Product $product2,
Product $product3
): int {
return $product1->price + $product2->price + $product3->price;
}
}
$totalPrice = Product::calculateTotalPrice($product1, $product2, $product3);
Productが$product1から$product3(そして今後作られるProductインスタンス)の集合であるという概念的な背景があるが、自身の集合から3つ要素を選び、その合計金額を計算するのは、集合の機能としては妥当ではないだろうか? これで1つ目の問題(インスタンスメソッドだと居心地がわるい問題)は解決だ。
2つ目の問題であったカプセル化毀損問題も同時に解決できる。もしも、priceがint型からCurrencyクラスになったとしても、その変更はProductクラスのコードだけで済むようになる。(もちろん、Product以外の他のインスタンスがpriceを使っていれば、そこにも変更が及ぶが、最大限変更の影響範囲をProductに留めることができる)
まとめ
- クラスはインスタンスの概念的な集合。
- 集合(クラス)に関する処理は静的メソッドが実装するにあたって良い場所である場合がある。
今後の話題
今回は触れなかったが、今後機会があれば触れたいこと:
- とはいえ、静的メソッドはできるだけ避けたほうがいい話。
- 静的メソッドの具体的なユースケースの話。