生命モデルとオブジェクト指向
昨日、Twitterで「性行為をオブジェクト指向で表現したらどうなるか」なんていう猥談をしていました。最初はふざけ半分で適当な事を言っていたのですが、考えていたら夢中になり、わりと中身のある話になりまして…
後になってその会話を読み返してみると、これがなかなか面白く、「生命が増殖するモデルってオブジェクト指向の題材にちょうど良いんじゃないか?」という事に気がお付いたわけです。
少し話が脇道にそれますが、自社で新人研修として、javaの講師になってオブジェクト指向を説明するかも、という話がありまして、そのための題材を探していたところだったので、早速実装してみる事にしましょう。
まず、基板になる「生命」のインターフェースを定義します。
/** * 生命を表すインターフェースです * @author tune * */ public interface Life { /** * そのインスタンスが死んでいればtrueを返します * @return */ public boolean isDead(); }
生命らしい活動と言えば、「増殖する」事でしょうか。
先程のLifeインターフェースを継承して、増殖能力を持った生命のインターフェースを定義します。
/** * 増殖可能な生命を表すインターフェースです * @author tune * */ public interface IncreasebleLife extends Life { /** * 増殖処理、自らの子孫を返します * @return */ public Life Increase(); }
ここで戻り値がLifeインターフェースのインスタンスである事に注意してください、IncreasebleLifeインターフェースではいけないのでしょうか。
色々と例え話はできるのですが、必ずしも子孫が「自ら増殖できる能力を持っているとは限らない」というという考え方ができます。具体的な一例は後ほど出てきますが、イメージの沸かない人は「色々なパターンに対応するため」とだけ考えて頂ければOKです。
さて、これで生死2パターンの状態を持ち、自ら増殖する能力を持った生命のインターフェースができたので、実際にこれを実装してみましょう。
クラス名はAmebaとし、3回分裂を繰り返すと死滅する事にします。
/** * 増殖する単細胞生物のクラスです * * @author tune * */ public class Ameba implements IncreasebleLife { private final int INCERASE_HESITATION = 3; //増殖できる回数制限 private int IncreaseCount = 0; private boolean Dead = false; //---------------------------------------------- @Override public Life Increase() { if(!Dead){ IncreaseCount++; if(IncreaseCount>=INCERASE_HESITATION) Dead = true; return new Ameba(); } return null; } //---------------------------------------------- @Override public boolean isDead() { return Dead; } }
このクラスは、Increaseメソッドを呼び出すと、新たなAmebaクラスのインスタンスを返却し(つまり、増殖する)、その回数が3回を超えると死滅します。
一度死んだAmebaクラスのインスタンスはそれ以上増殖する事はできないので、それ以降nullを返却する事になります。
簡単なケースなので、コードを見れば大体の動作はイメージできると思います。
さて、ここまでは単なる肩慣らしです。
もっと複雑な例に挑戦してみる事にします。即ち、オスとメスの交尾によって新たな子孫を生み出すモデルを、オブジェクト指向的なアプローチから実装してみる事にしましょう。
ところで、どんな複雑な体系であっても、それが「生命」である以上、先ほど作ったLifeインターフェースで定義したように、生死の状態を持ちます。また子孫をを生み出すという事は増殖する能力を有すると言えますので、IncreaseLifeインターフェースに属すると言えます。
こうする事によるメリットは色々あるのですが、アメーバだろうが犬だろうが猫だろうが、IncreaseLifeインターフェースを実装していれば、同じ方法で増殖(出産)させる事ができるようになりプログラムの拡張性が高くなります
哺乳類が出産する事を「増殖」と表現するのは酷く乱暴な気がしますが、これがオブジェクト指向でポリモーフィズムと言われる考え方です。(蛇足ですが、こうやって表現すると、人間も虫も同じ命なんだなぁと実感できる気がしませんか?)
では早速、「オス」と「メス」と「交尾」について定義してみましょう。
- オスはメスに自らの遺伝子情報を渡します
- メスはオスから遺伝子情報を受け取ります
- メスはオスから受け取った遺伝子情報が、同じ種族の場合、子を宿します
- 子は、自らと同じ種族のオス、またはメスで、その確率は50%とします
- メスは子を宿している場合、子を体外へ排出できます(出産)
まず、オスとメスの間で受け渡しされる「遺伝子情報」について定義しましょう。遺伝子情報と言ってもあまり複雑にし過ぎると理解できないので、単純に種族名を文字列として保持する小さなクラスとします。
/** * 遺伝子情報を表すクラスです * @author tune * */ public class DNA { private String FamilyName; /** * 種族名を設定します * @param FamilyName */ public DNA(String FamilyName){ this.FamilyName = FamilyName; } /** * 種族名を取得します * @return */ public String getFamilyName(){ return FamilyName; } }
人の遺伝子が受け渡しされる間に狼の遺伝子に変質してたら困るので、種族の情報はインスタンス生成時に一度だけ設定できるようにしましょう。
続いて、オスとメス、それぞれを表すインターフェースを定義します。
/** * 動物のオスを表すインターフェースです * @author tune * */ public interface Male extends Life { /** * 遺伝子情報を取得します * @return */ public DNA getDNA(); }
/** * メスを意味するインターフェースです * @author tune * */ public interface Scalpel extends IncreasebleLife{ /** * オスからDNAを受け取るメソッドです * @param maleDNA */ public void catchDNA(DNA maleDNA); }
そして、これらのインターフェースを元に、抽象クラスを作成します。
/** * オスの動物を表す抽象クラスです * @author tune * */ public abstract class MaleAnimal implements Male { protected String FamilyName = null; protected boolean Dead = false; //---------------------------------------------- @Override public DNA getDNA(){ if(!Dead) return new DNA(FamilyName); return null; } //---------------------------------------------- @Override public boolean isDead(){ return Dead; } }
import java.util.*; /** * メスの動物を表す抽象クラスです * @author tune * */ public abstract class ScalpelAnimal implements Scalpel{ protected String FamilyName = null; protected boolean Dead = false; protected Life Baby = null; //赤ちゃん //---------------------------------------------- @Override public void catchDNA(DNA maleDNA){ //受け取った遺伝子情報が別の種族のものの場合は何も起こらない if(!maleDNA.getFamilyName() .equals(FamilyName)) return; //同種の遺伝子ならオスかメスかどちらかの子供ができる if(new Random().nextBoolean()) Baby = createMaleBaby(); else Baby = createScalpelBaby(); } /** * オスの赤ちゃんを取得します * @return */ abstract protected Life createMaleBaby(); /** * メスの赤ちゃんを取得します * @return */ abstract protected Life createScalpelBaby(); //---------------------------------------------- @Override public Life Increase(){ //お産、赤ちゃんを体外に出す Life tmpBaby = Baby; Baby = null; return tmpBaby; } //---------------------------------------------- @Override public boolean isDead(){ return Dead; } }
こうして見ると、メスの体内ではオスなんかより全然複雑な処理をしているのが解りますね。女性には頭が上がらないです。
それはさておき、何故直接抽象クラスを作らずに、インターフェースを用意したのでしょうか。
これもポリモーフィズムです、次のような例を考えてみてください。
例えばミミズやカタツムリのように、明確な性別が決まっていないような生き物(所謂雌雄同体)を定義する場合、この抽象クラスは利用できません。何故なら、生まれてくる子供は半々の確率でオスかメスかが決まってしまっているからです。
しかし、Maleインターフェースと、Scalpelインターフェースをそれぞれ実装した雌雄同体抽象クラスを新たに作れば、雌雄同体を定義する事ができますし、そのクラスのインスタンスは、オスとメスが明確に分かれている動物と同じように扱う事ができるのです。
(本当は、先の猥談の中で「男の娘も定義できるようにしたい」という要望があったからなのですが・・・)
次に、このオス、メスそれぞれの抽象クラスを継承して、実際に動作するクラスを作るわけですが、これを人間にしてしまうとちょっと生々しいので、よくオブジェクト指向の説明で無理やり鳴かされている、猫のクラスを実装する事にします。
/** * オスの猫を表すクラスです * @author tune * */ public class MaleCat extends MaleAnimal { /** * コンストラクタでは種族名を定義します */ public MaleCat(){ FamilyName = "cat"; } }
/** * メスの猫を表すクラスです * @author tune * */ public class ScalpelCat extends ScalpelAnimal { /** * コンストラクタでは種族名を定義します */ public ScalpelCat(){ FamilyName = "cat"; } @Override protected Life createMaleBaby() { return new MaleCat(); //オス猫の赤ちゃん } @Override protected Life createScalpelBaby() { return new ScalpelCat(); //メス猫の赤ちゃん } }
種族名がハードコーディングになってしまっているのは、とりあえず今回はよしとしましょう。(本当はダメです、catインターフェース内にfinal変数を定義して実装する等の対処が必要です。)
オス猫の場合は種族名だけ定義すれば良いですが、メス猫の場合、抽象クラスの段階では子孫のクラスが不明確だったので、ここで明確にオス猫かメス猫が生まれるように実装します。
同じようにして犬のクラスや人間のクラスを定義する事ができますし、それらのクラスは猫と同じように扱う事ができます。
そして、その拡張のためには猫クラスに手を加える必要もありませんし、動物抽象クラスに手を加える必要もありません。
理想論を述べると、猫のクラスや動物の抽象クラス、その元となるインターフェース等に「手を加えるべきではありません」。
こういった設計を行う場合、機能拡張の必要性が生じた場合、外部での拡張は容易に行えるようにし、内部での拡張はしないようにするべき。という原則が、オブジェクト指向の考え方にはあり、この事を「開放閉鎖原則」といいます。
しかし実際にはクラスやインターフェース自体を書き換えたほうが良い事もあります。これはきつく言えば初期の設計不足という事になるのですが、実際にこういった修正が起こらないような設計を最初から完璧にする事は、不可能とは言わないまでもかなり難しいと言えます。
例えば今回の例で、「双子や三つ子が産めるようにしたい」という要件がでた場合、IncreasebleLifeインターフェースのIncreaseメソッドがArrayList
もし、この双子や三つ子を生むメソッドをScalpelCat抽象クラスに追加すれば良いと考えた人が居た場合、その考え方は改めるべきでしょう、世の言う「スパゲッティプログラム」はそうやって生まれていくわけです。
最後に、このクラスを実際に動作させるサンプルを提示してこの記事を締めくくりたいと思います。もし、この記事でオブジェクト指向への理解を深める目的で読まれた方がいましたら、先ほど例の上げた「雌雄同体抽象クラス」と「カタツムリのクラス」を考えてみると、良いのでは無いでしょうか。また、このままだと猫は不老不死なので、寿命が来たら死を迎えるように、作り直すのも良いでしょう。ポリモーフィズムを意識するようにしてみてください、どこから書きなおせば良いでしょうか。
/** * 猫クラスのテスト * @author tune * */ public class Sample { public static void main(String[] args){ Male myMale = new MaleCat(); //オス猫 Scalpel myScalpel = new ScalpelCat(); //メス猫 //メス猫の中には誰もいませんよ DisplayBabyClass(myScalpel.Increase()); //オス猫の遺伝子をメス猫が受け取る myScalpel.catchDNA(myMale.getDNA()); //お産 DisplayBabyClass(myScalpel.Increase()); //メス猫の中には誰もいなくなりました DisplayBabyClass(myScalpel.Increase()); } /** * 赤ちゃんのクラス情報を表示します * @param Baby */ private static void DisplayBabyClass(Life Baby){ if(Baby==null) System.out.println("Baby = null"); else System.out.println("Baby = "+Baby.getClass().getName()); } }