会社で似たような問題にドメイン駆動で開発していたらぶち当たったので、書いてみる。
部品と部品置き場の関係。 この両者には明らかに関係があって、ある部品はある部品置き場にしか置いちゃいけない。 例えば、タイヤは必ずタイヤ置き場に置かなきゃいけないし、ミラーやライトなんかは絶対に置いちゃいけない。 この決まりを破ると上司に怒られてしまうのだ。
Javaでコードにするとこう。
class Tire {} class TireRepository { public void put(Tire t) { ...; } } class Mirror {} class MirrorRepository { public void put(Mirror m) { ...; } }
このままでもいいけど、この部品と工場の関係はそれぞれで一緒。将来的に考えて、この関係を使って何かしたいかもしれないし、共通の処理を作る可能性も考慮して、抽象クラス化したいと考える。
abstract class Parts {} abstract class PartsRepository { public abstract void put(Parts p); }
これらをTireとMirrorでそれぞれ継承してやればよさそうだけど・・・
class Tire extends Parts {} class TireRepository extends PartsRepository { public void put(Tire t) { ...; } }
上のようにやると、putメソッドのオーバーライドにはならず、別な引数でのオーバーロードになってしまう。そのため、abstract で宣言されてるput(Parts p)を実装していないため、コンパイル時エラーになってしまう。もし無理やり実装するならこうだろうか。
class Tire extends Parts {} class TireRepository extends PartsRepository { /** @Override */ public void put(Parts p) { ...; } public void put(Tire t) { this.put( (Parts)t ); } }
こうしてやれば、put(Tire t)の方で呼ばれても、オーバーライドした方のメソッドで呼び出すことができ、だいたい意図通りに動く(気がする)。とはいえこれはエレガントじゃないし、部品と置き場を増やすたびにいちいちこのコードを書かなきゃいけないのはだるい。
もしくは、具象クラスのputメソッドで引数をTireにするのを諦めるという選択肢もある。しかし、これでは他のParts(例えばMirror)もTireRepositoryに置けてしまい、それがコンパイル時ではなく実行時のエラーになってしまうことは容易に想像できる。
実はこの問題にぶち当たったのはPHPで、動的言語でありオーバーロードもできないPHPでは後者の解しかなく半ば詰んでいたのだが、Javaならジェネリクスを使うことで以下のように解決できる。
abstract class Parts {} abstract class PartsRepository<P extends Parts> { public abstract void put(P p); } class Tire extends Parts {} class TireRepository extends PartsRepository<Tire> { /** @Override */ public void put(Tire t) { ...; } } class Mirror extends Parts {} class MirrorRepository extends PartsRepository<Mirror> { /** @Override */ public void put(Mirror m) { ...; } }
ジェネリクスを使うことで、PartsRepository抽象クラスに、Partsを継承したPと関連付けられているという制約を与えることができる。これで部品と置き場の関係は、これらを継承することで常に正しくなるし、将来的に部品に共通のメソッドを書くことも、それをオーバーライドすることも、更にそれを置き場の共通のメソッドで呼び出すこともできる。
ジェネリクスというと、Java初級者くらいの頃は、List
さて、Javaであれば上で解決なのだけど、問題は今使っているのがPHPなのだ。タイプヒンティングつけるのを諦めるしか無いのかなあ。