1. 抽象化の概念
要するに:抽象化とは、複雑なものをシンプルに捉える技術です。
プログラミングにおける抽象化とは、オブジェクト群から共通する特性や振る舞いを取り出し、個別の詳細(内部の仕組みがどうなっているかなど)は無視することです。都市の地図を描くのを想像してください。道・建物・川は載っていても、各窓のカーテンの色のような細部は載りません。地図は都市の抽象化です。
OOP における抽象化とは、課題にとって重要な性質と行動だけを表し、不必要な詳細を隠すようなクラスやインターフェースを作ることです。
身近な例
- 乗り物は抽象です。バス・自転車・宇宙船かどうかは関係なく、移動できる、乗客や運転者がいるといった共通点があります。
- 動物も抽象です。すべての動物は呼吸し、食べ、動きますが、そのやり方は種によって異なります。
- 支払いは銀行系ソフトの抽象です。カードなのか、PayPal なのか、仮想通貨なのかは常に気にしません。重要なのは、処理できて結果が得られることです。
なぜ抽象化が重要なのか?
- 目の前の課題に不要な詳細を考えなくて済みます。
- 拡張・保守がしやすいシステムを設計できます。
- 具体的な実装を意識せず、共通インターフェースを通じてオブジェクトを扱えます。
2. Java における抽象化
Java では、抽象化は主に次の 2 つの手段で実現されます。
- 抽象クラス(abstract class)
- インターフェース(interface)
この講義では抽象クラスに焦点を当てます。(インターフェースはすぐ後で扱います!)
抽象クラスは、直接インスタンス化することを意図しないクラスです。派生クラスのための共通の土台(テンプレート)を定めます。抽象クラスには、実装済みのメソッド(本体あり)と抽象メソッド(本体なし)を定義できます。抽象メソッドは、派生クラスで必ず実装しなければなりません。
抽象メソッドは実装、つまり本体のないメソッドです。「このクラスのすべての子孫は、このメソッドをそれぞれのやり方で実装せよ」と指示します。
例:「図形」という抽象
public abstract class Shape {
public abstract void draw(); // 抽象メソッド — 本体なし!
}
ここでは「すべての図形は描けるが、どう描くかは知らない — 各サブクラスが決める」と宣言しています。
なぜ実装の詳細を必ずしも知る必要がないのか?
抽象化を使うと、オブジェクトの「表」(必ず備えるべきメソッド群)を通じて操作します。メソッドの中身がどう動くかは重要ではありません。
たとえば、支払いを処理するためにオブジェクトへ payment.process() を呼びます。実装がどうであれ、動けばよいのです。これにより次が可能になります。
- その実装を使う側のコードを書き直さずに、ある実装を別の実装へ差し替えられる。
- テストが容易になる(「スタブ」実装に差し替えられる)。
- コードが柔軟になり、変更に強くなる。
3. 抽象化の利点
設計と保守の単純化
抽象化により余計なことを考えずに済みます。車のエンジンの仕組みを知らなくても、ハンドルとペダル、そして「前へ進む」という操作だけで運転できます。コードでも同じです。抽象クラスやインターフェースを相手にすれば、必要なものだけが見えます。
システムの拡張が容易
Shape のような抽象クラスがあれば、新しい図形タイプ(たとえば Triangle)を、既存コードを変えずに追加できます。
コンポーネント間の結合度の低減
プログラムの各部が抽象を介してのみやりとりするなら、互いに独立して変更できます。コンセントとプラグのように、規格が合っていればどの機器でも差し込めます。
4. 実用例
抽象化が実際にどう見えるかを見ていきましょう。学習用アプリに組み込めるサンプルを使うので、手を動かして確かめられます。
例 1: クラス「Shape」(図形)
public abstract class Shape {
public abstract void draw();
}
いくつか具体的な図形を作ります。
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("円を描画します");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("長方形を描画します");
}
}
抽象を使ってみます。
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 円を描画します
s2.draw(); // 長方形を描画します
ここでは変数の型は Shape です。中身がどの図形かは関係ありません。これこそ抽象化の力です!
例 2: クラス「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 p : payments) {
p.process();
}
その結果、各支払いはそれぞれのやり方で処理されますが、それを呼び出す側のコードは気にする必要がありません。
5. 抽象化と実装詳細:混同しないために
抽象化は「何を」であって「どのように」ではない
抽象を設計するときは、「オブジェクトは何ができるべきか?」に答えます。
実装の詳細は「それをどうやって行うか?」です。
例えば:
- 抽象化:「どの図形も描画できるべきだ(draw())。」
- 実装の詳細:「円は円周で描き、長方形は 4 本の線で描く。」
身近なアナロジー
テレビのリモコンを思い出してください。信号をどう送るかは気にしません。重要なのは「電源」「チャンネル切替」「音量アップ」といったボタンがあることです。これが抽象化、つまりあなたが押せるボタンの集合です。一方、リモコンを設計するエンジニアは実装の詳細を考える必要があります。
6. プログラムで抽象を見つける方法
ステップ 1. 共通点を見つける
対象ドメインのオブジェクトを眺め、何が共通かを探します。例えば、乗り物はすべて走れる、動物はすべて食べられる、支払いはすべて処理できる、などです。
ステップ 2. 抽象クラスを定義する
全オブジェクトに共通なものだけを含む抽象クラスを作成します。
public abstract class Transport {
public abstract void move();
}
ステップ 3. サブクラスで詳細を実装する
public class Car extends Transport {
@Override
public void move() {
System.out.println("車は道路を走ります");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("自転車はペダルをこぎます");
}
}
ステップ 4. コードで抽象を使う
Transport[] transports = {
new Car(),
new Bicycle()
};
for (Transport t : transports) {
t.move();
}
あなたのコードは抽象に依存して動作し、実装の詳細に依存しません。
7. 抽象化と詳細化のバランス
初心者にありがちな誤り — 抽象を細かくし過ぎる、あるいは逆に抽象が漠然とし過ぎること。
- 抽象が漠然とし過ぎる(例えば「Object」クラスに「doSomething」メソッドだけ)と、何の役にも立ちません。
- 抽象が細か過ぎる(例えば「赤い縁取りで半径 5 の円」)と、もはや抽象の意味が薄れ、具体クラスを書いたほうが簡単です。
黄金律:抽象は、本当に共通で、かつあなたの課題にとって重要なことだけを表すべきです。
8. 抽象化でよくあるミス
エラー No.1: 抽象クラスのインスタンス化を試みる。
抽象クラスは直接インスタンス化するものではありません。もし Shape s = new Shape(); と書けば、コンパイラは次のエラーを出します:Cannot instantiate the type Shape。これは、店でただの「乗り物」を買おうとするようなものです。できません。具体的な自転車・車・バスから選ぶ必要があります。
エラー No.2: 派生クラスで抽象メソッドの実装を忘れる。
クラスが抽象クラスを継承し、すべての抽象メソッドを実装しない場合、そのクラス自身も抽象になり、インスタンス化できません。必須メソッドをすべて実装したか確認してください。
エラー No.3: 抽象と実装詳細の混在。
特定のサブクラスだけに必要な詳細を抽象クラスへ入れ始めたら、それは悪い抽象の兆候です。例えば、抽象クラス Payment に cardNumber フィールドが現れるような場合です。すべての支払いがカード経由とは限りません。
エラー No.4: 抽象化し過ぎ。
抽象化のための抽象化は避けましょう。もしシステムに 1 種類のオブジェクトしかなく、将来も拡張されないなら、抽象化はコードを複雑にするだけです。
GO TO FULL VERSION