1. 階層の構築: 抽象から詳細へ
抽象と階層の実装は、一般的な規則から具体的な詳細へとコードを構造化する方法です。まず、すべてのオブジェクトが何をできるべきか(抽象)を記述し、その後で各具象クラスがそれをどのように行うかを具体化します。
プログラミングでも日常と同じく、すべては問いから始まります。例えば: 「円と長方形の共通点は何でしょうか?」 答え: どちらも図形です。では図形の共通点は? たいてい面積があり、描画できます。
Java ではこれを abstract クラスで表現します:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
ここで言っていること:
- どの図形も自分の面積を計算できなければなりません(area())。
- どの図形も描画できなければなりません(draw())。
- それをどのように実現するかは(今のところ)問いません。
では具体的な図形を作りましょう:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("半径 " + radius + " の円を描画します");
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("長方形 " + width + "x" + height + " を描画します");
}
}
何をしたのか?
- 共通部分を抽象クラスに切り出しました。
- 振る舞いをサブクラスで具体化しました。
概略図:
. Shape
/ \
Circle Rectangle
表: どこで何が実装されているか
| クラス | area() | draw() | 独自のフィールド |
|---|---|---|---|
| Shape | |
|
- |
| Circle | 実装済み | 実装済み | |
| Rectangle | 実装済み | 実装済み | |
2. なぜ便利なのか(そもそもなぜ動くのか)
異なるオブジェクトを同じインターフェースで扱える
たとえば、図形のコレクションがあるとします:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
型を意識せずに同じように反復できます:
for (Shape shape : shapes) {
shape.draw();
System.out.println("面積: " + shape.area());
}
どれが円でどれが長方形かは JVM に任せましょう! これがポリモーフィズム(多態性)です(すでに触れましたが、次のブロックでさらに詳しく扱います)。
拡張が容易
三角形を追加したくなったら? 新しい型を書く(既存コードは変更不要)だけです: Triangle は extends Shape。
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("三角形を描画します: 底辺 " + base + "、高さ " + height);
}
}
他のコード(例えば図形リストの走査)は変更不要です。
コードの重複を避ける
もしすべての図形に共通のプロパティ(例えば色)が必要になったら、それを抽象クラスに切り出すと便利です:
public abstract class Shape {
private String color = "黒";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
これで、円でも三角形でも、すべてのサブクラスが色を「継承」によって手に入れます。
3. 実践: ミニグラフィックエディタを開発する
すべてを組み合わせてみましょう。簡単なグラフィックエディタを作ると想像してください。
抽象クラス Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
具体的な図形
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("長さ " + length + " の線を色 " + getColor() + " で描画します");
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("線の新しい長さ: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("楕円を描画します: 軸 " + a + " と " + b + "、色 " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("楕円の新しい軸: " + a + "、" + b);
}
}
新しいツール(例えば Polygon)を追加したいですか? 新しいクラスを作るだけで OK — エディタのコードは抽象の Figure 経由で動作します。
コードでの使用
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
出力:
長さ 10.0 の線を色 red で描画します
線の新しい長さ: 15.0
楕円を描画します: 軸 5.0 と 3.0、色 red
楕円の新しい軸: 7.5、4.5
階層の可視化
. Figure
/ \
Line Ellipse
4. 重複を避けるには: 共通のフィールドとメソッド
サブクラス間で共通なのはメソッドだけでなくフィールド(例: 中心座標)の場合もあります。抽象クラスはそれらを置くのに最適な場所です:
public abstract class Figure {
private double x, y; // 中心の座標
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("図形を点 (" + x + ", " + y + ") に移動しました");
}
public abstract void draw();
}
これで Line や Ellipse は、このメソッドを再実装することなく移動できます。
5. 別の例: 決済システム
抽象は図形だけの話ではありません。たとえば、決済処理システムを作っているとします.
抽象クラス Payment
public abstract class Payment {
public abstract void process();
}
具体的な実装
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("クレジットカードでの支払いを処理します");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("PayPal での支払いを処理します");
}
}
使用方法
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
出力:
クレジットカードでの支払いを処理します
PayPal での支払いを処理します
6. このアプローチの利点
- 統一インターフェース: 異なるオブジェクトを同じやり方で扱える。
- 拡張性: 新しい種類のオブジェクトを追加しても既存コードの書き換えが不要。
- 重複の最小化: 共通部分は基底の抽象クラスに集約。
- 柔軟性: 詳細を気にせず抽象型のコレクションを扱える。
7. 実例: 交通
抽象は教科書だけの話ではありません。たとえば、輸送管理システムを設計するとします:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
具体的な輸送手段が詳細を実装します:
public class Car extends Transport {
@Override
public void move() {
System.out.println("車は道路を走ります");
}
@Override
public void fuelUp() {
System.out.println("ガソリンを給油します");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("自転車はペダルをこぎます");
}
@Override
public void fuelUp() {
System.out.println("自転車に燃料は不要。必要なのはサイクリストのサンドイッチだけ!");
}
}
8. 役に立つ図式: 抽象の階層の作り方
[抽象クラス]
|
[具体的なサブクラス]
|
[さらに具体的なサブクラス] (必要なら)
- 共通のものは上へ!
- 固有のものは下へ!
9. 抽象と階層を実装する際の典型的な誤り
エラー No. 1: サブクラス間のコード重複。
各サブクラスで同じフィールドやメソッドを書いていると気づいたら、それは抽象クラスに切り出すサインです。重複が減るなら、抽象を「広げる」ことを恐れないでください。
エラー No. 2: 「一般から個別へ」の原則に反する設計。
初心者はしばしば詳細から作り始め、共通部分を忘れがちです。その結果、RedCircleWithShadow のような、全体構造にうまく当てはまらないクラスが生まれます。まず抽象を定めてから詳細を詰めましょう。
エラー No. 3: 階層が深すぎる。
継承の連鎖が 3〜4 段以上になっているなら、継承の代わりに合成やインターフェースの利用を検討しましょう。
エラー No. 4: 不適切なメソッドの強制実装。
抽象クラスに、あるサブクラスには無関係な抽象メソッドが多すぎる場合、構造の見直しを検討しましょう。例えば、すべての交通手段に fuelUp() が必要なわけではありません(自転車には不要です)。
エラー No. 5: 抽象クラスとインターフェースの混同。
抽象クラスは共通状態や部分的な実装がある場合に使います。インターフェースはメソッドの存在だけを約束したい(データを保持したり振る舞いを実装したりしない)場合に使います。不必要にこれらを混同しないでください。
GO TO FULL VERSION