1. 実際のアプリケーションにおける抽象化: なぜ必要か
これまでの講義で抽象化の概念と簡単な例を見てきました。ここでは、より現実的な課題でこのアプローチがどのように機能するかを確認します。 実務のプロジェクトでは、異なるが似通ったオブジェクトを扱うことがほとんどです。たとえば、さまざまな支払い方法、さまざまな交通手段、グラフィックエディタのさまざまな図形です。抽象化を使わないと、コードはすぐに「if-else」の羅列とコピペの塊になります。抽象化を使えば、整然として美しく、何より拡張や保守がしやすくなります。
抽象化の効用:
- 実装の詳細を隠せる: オブジェクトの内部構造を気にせず、共通のインターフェース越しに扱える。
- コードの重複を避けられる: 共通の振る舞いやフィールドを基底クラスに集約できる。
- システムを容易に拡張できる: 新しい種類のオブジェクトを追加しても既存コードを書き換えずに済む。
- コードを柔軟にできる: 実装を差し替えても他の部分を変更せずに済む。
いくつかの分野別の例を見ていきましょう。
2. 例 1: 決済システム
課題
あなたはオンラインショップのモジュールを実装しています。タスクは異なる種類の支払い(クレジットカード、PayPal、暗号資産)を処理することです。どれも「支払いを処理する」必要がありますが、詳細はそれぞれ異なります。
抽象化: クラス Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// 抽象メソッド: 決済の処理方法はサブクラスが決める
public abstract void process();
// すべての決済に共通のメソッド
public void printAmount() {
System.out.println("支払金額: " + amount + " RUB");
}
}
具体的な実装
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("カード決済を処理中: " + cardNumber);
// ここに銀行との連携コードが入るはず :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("PayPal 決済をこのアカウントで処理中: " + email);
// ここでは PayPal API を呼び出す
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("暗号通貨の支払いをウォレットへ処理中: " + walletAddress);
// ここにはブロックチェーンの魔法があるかも
}
}
抽象化の利用
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
実行結果:
支払金額: 1500.0 RUB
カード決済を処理中: 1234 5678 9012 3456
---
支払金額: 500.0 RUB
PayPal 決済をこのアカウントで処理中: user@example.com
---
支払金額: 0.05 RUB
暗号通貨の支払いをウォレットへ処理中: 0xABCD...
---
利点:
- 既存コードを変えずに新しい支払い方法(例: Apple Pay)を追加できる。
- 決済を扱うコードは具体的なタイプに依存しない。
- 共通ロジック(例: printAmount() で金額を出力)は1か所に実装される。
3. 例 2: 交通手段
課題
ゲームやシミュレータには、車、自転車、列車などさまざまな交通手段があります。いずれも「移動」できますが、その方法は異なります。給油が必要なものもあれば、不要なものもあります。
抽象化: クラス Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// すべての乗り物が給油を必要とするわけではない。デフォルトでは不要とする
public void fuelUp() {
System.out.println(name + ": 給油は不要です。");
}
}
具体的な実装
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " は道路を走っています。");
}
@Override
public void fuelUp() {
System.out.println(name + ": ガソリンを給油します。");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " はペダルをこいでいます。");
}
// fuelUp はオーバーライドしない — 自転車に給油は不要
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " は線路の上を疾走しています。");
}
@Override
public void fuelUp() {
System.out.println(name + ": ディーゼルまたは電力で補給します。");
}
}
抽象化の利用
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Stels"),
new Train("Sapsan")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
実行結果:
Toyota は道路を走っています。
Toyota: ガソリンを給油します。
---
Stels はペダルをこいでいます。
Stels: 給油は不要です。
---
Sapsan は線路の上を疾走しています。
Sapsan: ディーゼルまたは電力で補給します。
---
利点:
- タイプを判定せず、どの乗り物も同じように処理できる。
- 新しい乗り物(例: 電動キックボード)を簡単に追加できる。
4. 例 3: グラフィックエディタ
課題
ミニ・グラフィックエディタを作成しているとします。そこには線分、楕円、多角形があり、いずれも「図形」です。図形は描画とサイズ変更ができますが、その実装は図形ごとに異なります。
抽象化: クラス Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
具体的な実装
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("長さ " + length + " の線を色 " + color + " で描画します");
}
@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 + " の楕円を色 " + color + " で描画します");
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("楕円の新しいサイズ: " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("辺の数が " + sides + " の多角形を色 " + color + " で描画します");
}
@Override
public void resize(double factor) {
System.out.println("スケール係数 " + factor + " で辺の数が " + sides + " の多角形のサイズを変更します");
}
}
抽象化の利用
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
実行結果:
長さ 10.0 の線を色 green で描画します
線の新しい長さ: 20.0
---
長径 5.0 と短径 3.0 の楕円を色 green で描画します
楕円の新しいサイズ: 10.0 x 6.0
---
辺の数が 6 の多角形を色 green で描画します
スケール係数 2.0 で辺の数が 6 の多角形のサイズを変更します
---
利点:
- すべての図形を同じリストに保持し、同じ流れで処理できる。
- 新しい図形(例: 星形やハート)を容易に追加できる。
- 共通メソッド(例: setColor() での色設定)は1回だけ実装すればよい。
5. 抽象化がコードを簡潔にする仕組み
上の各例には共通のパターンがあります:
- 基底の抽象クラスが契約(オブジェクトが何をできるか)を定義する。
- 具体的なサブクラスが詳細を実装する。
- 抽象に対して書かれたコードはオブジェクトの型に依存せず、システムを柔軟かつ拡張可能にする。
アプローチ比較表
| 抽象化なし(if-else) | 抽象化あり(OOP) |
|---|---|
| 型ごとの条件分岐が多い | 新しい型 — コードを変更する |
| ロジックの重複 | ロジックは1か所に集約 |
| 拡張が難しい | 追加が容易 |
| テストしづらい | 実装の差し替えが容易 |
6. 抽象設計でよくある誤り
よくある誤り1: 抽象化のための抽象化。
オブジェクトが1種類しかなく拡張予定がないなら、抽象クラスは不要です。理由なくコードを複雑にしないでください。
よくある誤り2: 抽象が抽象的すぎる。
基底クラスがあいまいすぎると、サブクラス同士が名前以外の共通点を持たなくなります。例えば、あらゆるものを表す「Thing」のような抽象です。これは保守や理解を難しくします。
よくある誤り3: サブクラス間のコード重複。
すべてのサブクラスで同じ実装になっているメソッドは、基底クラスに移して抽象でなくすべきです。
よくある誤り4: 「一般から特殊へ」の原則違反。
抽象クラスに特定のサブクラスだけが必要とする詳細が現れたら、その抽象の切り方は誤りです。
よくある誤り5: 抽象メソッドの実装漏れ。
サブクラスで抽象メソッドをすべて実装しないと、コンパイラはそのクラスも抽象クラスにするよう要求します。ときにこれは予想外です :)
GO TO FULL VERSION