5分でわかるオブジェクト指向
プログラミングを勉強していて、オブジェクト指向でつまる人は多い。その理由は、実際のプログラミングでどのようにオブジェクト指向を使うかという、わかりやすい例が示されていないからだと思う。この記事では、必要最小限の実例でオブジェクト指向の使い方を説明する。
※ この記事はクラス、メソッド、フィールド、コンストラクタ、継承など、一度は勉強したけれど、オブジェクト指向がいまいちよくわからない、どのように使っていいのかわからないという人を対象としています。なお、言語はJavaで書いています。
生徒一覧、社員データベースなど、人のデータを扱うことは多い。ここでは、極力単純化して、姓(familyName)と名(firstName)と年齢(age)しかデータを持たないPersonクラスを考えてみよう。
Personのフルネームを取得することは非常に多い。毎回、姓、名と並べて表示するプログラムを書くよりも、フルネームを返すメソッドgetFullNameをPersonクラスに作っておいた方がよさそうだ。
でも、日本人なら姓、名の順のフルネームを書くけど、アメリカ人は名、姓の順にフルネームを書く。しかたがないのでJapanesePersonとAmericanPersonの二つのクラスを作成しよう。
JapanesePersonとAmericanPersonは、getFullNameメソッド以外の部分は共通のはずだ。同じことを2回書くのは面倒(変更があった場合に片方だけ直し忘れるなど、保守性の問題もある)なので、共通部分はPersonクラスで作ってしまい、JapanesePersonとAmericanPersonではPersonクラスを継承してgetFullNameメソッドだけを作ろう。
しかし、これだとPersonを継承したクラスにgetFullNameメソッドが作られるかは、それを作る人次第だ。Personを継承したクラスではgetFullNameメソッドを持つことを強制したい!!
どうするか。Personには、getFullNameの振る舞い(姓、名なのか、名、姓なのか)は書けない。書けるのは、Stringでフルネームを返すgetFullName()というメソッドがあることだけだ。中身がない(振る舞いを記述しない)メソッドは作れないのか。
このような、中身のないメソッドを抽象メソッド(abstractメソッド)と呼ぶ。
public abstract String getFullName();
というように書く。
抽象メソッドを持つクラスは抽象クラス(abstractクラス)として定義しなければならない。classの前にabstractをつければいい。抽象クラスのインスタンスは生成することができない(new Personができない)。抽象メソッドが呼ばれたときに困ったことになるからだ。
さて、これが今やりたいことだ。これに従って作ったプログラムが↓だ。上の説明は最小限のものなので、ソースコードをよく読んで理解してほしい。特にカプセル化とポリモーフィズムは重要だ(この二つは文章で説明するよりも、コードを見た方がわかりやすい)。
// 姓、名、年齢を持ったクラスPerson。getFullNameメソッドでフルネームを取得できる。 // フルネームを姓、名の順に書くか、名、姓の順に書くかは国によって違うため、 // getFullNameメソッドは抽象メソッドとして定義しておき、子クラスで実装する。 abstract class Person { // フィールドはpublicにしない。これはフィールドに直接アクセスできないようにするため。 // フィールドへのアクセスはアクセサメソッド(getter、setter)を用いて行う。(カプセル化) // 今回はフィールド子クラスからアクセスする必要があるので、privateではなくprotectedにする。 protected String firstName; // 姓 protected String familyName; // 名 protected int age; // 年齢 public Person(String firstName, String familyName, int age) { this.firstName = firstName; this.familyName = familyName; this.age = age; } // アクセサメソッド(getter)を使ってフィールドの値を得る。 public String getFirstName() { return this.firstName; } // アクセサメソッド(setter)を使ってフィールドの値を書き換える。 public void setFirstName(String firstName) { this.firstName = firstName; } public String getFamilyName() { return this.familyName; } public void setFamilyName(String familyName) { this.familyName = familyName; } public int getAge() { return this.age; } // setterで不正な値をチェックして対処する。 // カプセル化していなければ、毎回チェックしなければならない。 public void setAge(int age) { if (age >= 0) { this.age = age; } else { // 負の値が与えられた場合は0歳にする。 this.age = 0; } } // getFullNameは引数なしでStringを返す、ということだけを定義する。 // 中身の実装に関しては、子クラスに任せる。 public abstract String getFullName(); } // 日本人を表すクラス。getFullNameは姓、名の順に並べた文字列を返す。Personクラスを継承。 class JapanesePerson extends Person { // superで親クラスのコンストラクタを呼び出す必要がある。 public JapanesePerson(String firstName, String familyName, int age) { super(firstName, familyName, age); } // 姓、名の順に並べた文字列を作成して返す。 public String getFullName() { return this.familyName + " " + this.firstName; } } //アメリカ人を表すクラス。getFullNameは名、姓の順に並べた文字列を返す。Personクラスを継承。 class AmericanPerson extends Person { public AmericanPerson(String firstName, String familyName, int age) { super(firstName, familyName, age); } // 名、姓の順に並べた文字列を作成して返す。 public String getFullName() { return this.firstName + " " + this.familyName; } } // mainメソッドを持ったクラス。 public class ObjectOrientedProgramming { public static void main(String[] args) { // 子クラスのインスタンスは親クラス型変数に代入できる。 // 子クラスは親クラスのメソッドをすべて持っているので // 親クラスのように振舞うことができる。 Person aoi = new JapanesePerson("優", "蒼井", 23); Person tom = new AmericanPerson("Tom", "Cruise", 46); // アクセサメソッド(setter)を用いてフィールド値の設定を行う。(カプセル化) aoi.setAge(22); // アクセサメソッド(getter)を用いてフィールド値を取得する。(カプセル化) System.out.println(aoi.getAge()); System.out.println(); // Person型変数の振る舞いはその実体によって決まる。(ポリモーフィズム) // aoiの実体はJapanesePerson、tomの実体はAmericanPerson。 System.out.println(aoi.getFullName()); System.out.println(tom.getFullName()); System.out.println(); // JapanesePersonとAmericanPersonの入り混じった、Person型配列も作れる。 Person[] persons = { new JapanesePerson("駿", "宮崎", 67), new AmericanPerson("Steven", "Spielberg", 61), new JapanesePerson("俊二", "岩井", 45) }; // JapanesePersonもAmericanPersonも同じPersonとして扱える。 // ポリモーフィズムで、それぞれの実体から適切にgetFullNameメソッドが呼ばれる。 for (int personIndex = 0; personIndex < persons.length; personIndex++) { // 実体がJapanesePersonなら姓、名の順に、AmericanPersonなら名、姓の順に表示。 System.out.println(persons[personIndex].getFullName()); } } }
実行結果は次のようになる。
オブジェクト指向の三本柱はよく、カプセル化、継承、ポリモーフィズムだと言われる。でも、本当に大事なのは、それらを使って実現できる抽象化だと思う。
上のプログラムは、その抽象化を簡単な例で示そうとしたものだ。このプログラムが、少しでもオブジェクト指向の理解に役立てば幸いだ。