概要
あるメソッドの処理を別のメソッドに変えることを継承で実現すると、メソッド名の変更をどう実装すればいいか悩む。
あるいはメソッド名が具体的すぎる問題。
もともとのメソッドをオーバーライドすれば、呼び出し側に変更は加えられないが、メソッド名が嘘になる。
別メソッド名を定義すると、呼び出し側の処理を書き換える必要が出て、メソッド名以外の部分は変更が無いのでコピペになる。
柔軟なアプローチが失われてる感。
コード
class Sample
{
protected $hello = 'Hello';
function getHello()
{
return $this->hello;
}
function hoge()
{
// いろいろ
$this->getHello();
// いろいろ
}
function huga()
{
// いろいろ2
$this->getHello();
// いろいろ2
}
// ほかいろいろ
}
たとえとして。
クラスはHello
という文字列を持ち、様々なメソッドからGetterで呼び出されている。1
文字列じゃなくてもいい。消費税とか、計算結果でもいい。タイムリーなら年号とかでもいい。
返した値はそのまま引数になったり加工されたりされる。
まあ肥ったクラスなんだけど、クラスじゃないよりはましになったなといった感じ。
getHello()
はそこそこ参照される。
getHello()
が'Hello'
を返すことは普通なら今後ほぼ変わらないはずなんだけど、カスタマイズ依頼が来た。
「'Hello'
じゃなくて'こんにちは'
にしてくれ。」
これはたとえなので、ローカライズの問題じゃないんだけど。
消費税が5%から8%になった。でもいい。
それを変えるとは思わなかったが、依頼内容はわからなくもない。
カスタマイズなので、全体はforkなりbranch切ったりで、カスタマイズに入る。
カスタマイズとは言え変更箇所は小さくしたい。
だいたい使う手法は(LSPを無視した)差分プログラミングだ。
変更対象のクラスを継承する。
変更箇所のメソッドはオーバーライドで書き換え、
機能追加のメソッドは新規に作成し、呼び出したい所にさっきと同じようにオーバーライドで書き換える。
その過程でいくつかprotected
にしなければならないだろうが、元のクラスに影響は少ない。
カスタマイズ内容だけが継承後のクラスにまとめられる…気がする。
あとはnew Sample
を継承したクラスに書き換えれば終わりだ。
class Custom extends Sample
{
}
さて、'こんにちは'
を入れようとして思う。
どこに入れればいいだろうか。
一番簡単なカスタマイズは、getHello()
が'こんにちは'
を返すことだ。
1~4行ぐらいで実現できるだろう。
class Custom extends Sample
{
protected $hello = 'こんにちは';
}
class Custom extends Sample
{
function getHello()
{
return 'こんにちは';
}
}
class Custom extends Sample
{
private $konnichiwa = 'こんにちは'
function getHello()
{
return $this->konnichiwa;
}
}
これで終わりだ。getHello()
を呼び出していた所に変更は不要。
カスタマイズの影響、修正範囲を小さくするならベターだけど、
getHello()
は嘘つきになる。'Hello'
が返ってくるように読めるのに、実際は'こんにちは'
だ。
たとえのせいでガマン出来そうに見えるけど、本当はもっと盛大な嘘になるので、自分が引き継ぎの立場か数年後にコードを見ると、怒ること間違い無し。
じゃあもっとちゃんとkonnichiwa
をプロパティにしたりしてgetKonnichiwa
を作ればどうなるか。
class Custom extends Sample
{
private $konnichiwa = 'こんにちは'
function getKonnichiwa()
{
return $this->konnichiwa;
}
function hoge()
{
// いろいろ
$this->getKonnichiwa();
// いろいろ
}
function huga()
{
// いろいろ2
$this->getKonnichiwa();
// いろいろ2
}
}
getHello()
をgetKonnichiwa()
にするために、getKonnichiwa()
以外同一の処理をわざわざコピペでオーバーライドしている。
コピペだし、将来Sample
の変更が自動反映される箇所が少なく同じ更新処理をこちらにも書かなければならなくなり、
自分が引き継ぎの立場か数年後にコードを見ると、怒ること間違い無し。
どうすればよかったの?
わからない。
getHello()
をもっと抽象的な名前で実装しておけばよかった。
たとえばgetGreeting()
というよりぼんやりとした名前なら返り値が'Hello'
でも'こんにちは'
でも違和感がなく、最初の変更方法で済んだと思う。
でもgetHello()
は一般的なアクセサ名だし、カスタマイズが来ることがあるとは言えどこに来るかはわからないので実装のときにそこまで拡張性は考えていない。
あるいは//いろいろ
の部分に直接処理を書かず、メソッド呼び出しだけにしてコントローラーみたいにすれば、メソッド呼び出しをコピペすることになっても呼び出しの変更以外の内部的な処理は自動で反映されることになる。と思う。
そこはメソッドをどれだけ小さくするかという問題だし、処理自体はかならずどこかには生まれるので…。先見の明になるのだろうか。
カスタマイズが来たという実績があるので、オリジナルのソースをgetGreeting()
にする改修をして、そこからカスタマイズに入ればスムーズに行くと思われる。
が、開発フローがそんなに柔軟な場合は少ないと思う。
カスタマイズのためのオリジナルの修正(更新)、の前例はないし、出来たとしてもオリジナルのリリースまで数ヶ月かかるので現実的ではない。(リリース済みのものでカスタマイズする)
何が正しいかはわからないけどとりあえずどうするの?
コピペが増えても正しい名前のgetKonnichiwa()
で実装します。
ウソ、ヨクナイ。
呼び出し箇所の前後の処理がある程度長いというのは自分の設計がわるかったと言うことで、やることは(勝手なやりたい前提、継承とか、ながらも)正しいそうな気がするから。
数カ所なら許容できたが、同様の問題が複数箇所に出ており、
数が増えてコントローラーのコピペまで必要になってきたので、
カスタマイズ側のコメントでフォローし、コントローラーなど呼び出し側の多少の嘘は部分部分で許容していく…。
IoCとは言っても呼び出し側のメソッド名指定までは動的には変わらないので、ファサードのようにコントローラー側は抽象的な名前を提供してモデル側のみであれこれ入れ替えられるほうが良かった。前章と同じことを書いているが、コントローラー触りたくない。
1行のメソッド名書き換えのためだけに継承してオーバーライドするのも大変なのに、コントローラーだとルーティングの上書きとかも出てくるので、あまりよろしくない…。
-
別に直接呼んでも良い ↩