本稿では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
に留めることができる)
まとめ
- クラスはインスタンスの概念的な集合。
- 集合(クラス)に関する処理は静的メソッドが実装するにあたって良い場所である場合がある。
今後の話題
今回は触れなかったが、今後機会があれば触れたいこと:
- とはいえ、静的メソッドはできるだけ避けたほうがいい話。
- 静的メソッドの具体的なユースケースの話。