- 技術 💻
抽象化とは?意味とプログラミングの例|3言語で段階実装し差分を比較
結論
抽象化とは、たくさんの具体的なものから共通する性質だけを取り出し、いまの目的に関係のない細部を捨てて扱いやすくすることです。プログラミングでは「細かい手順を関数・インターフェース・抽象クラスの後ろに隠し、使う側は約束(名前と引数)だけを見ればよい状態にすること」を指します。
この記事でわかること
- 抽象化の意味を日常の例と、捨象・一般化・具体化との違いで
- 同じ通知処理を Python・Java・TypeScript で「関数→インターフェース→抽象クラス」と段階的に書いて実行した結果
- 変更の差分行数・テストの書きやすさ・抽象化しすぎの害(早すぎる抽象化・漏れのある抽象化)の実測
抽象化は「ぼかすこと」ではなく、見る範囲を決めて、それ以外を隠すことです。プログラムでは、下の図のように何段もの抽象化が重なっていて、ふだん書くコードはその一番上に乗っています。
flowchart TD
A[アプリのコード<br>notify order] --> B[ライブラリ・API<br>print や open]
B --> C[言語の実行環境<br>Python・JVM・Node.js]
C --> D[OS のシステムコール<br>write・openat]
D --> E[CPU の機械語と<br>ハードウェア]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| 抽象化とは(意味) | 共通点を取り出し、目的に関係のない細部を捨てて扱うこと | 抽象化とは |
| 抽象化と具体化の違い | 抽象化はまとめる方向、具体化は細かくする方向。往復して使う | 抽象化とは |
| 抽象化 プログラミングでの意味 | 手順やデータの中身を隠し、名前と約束だけで使えるようにすること | プログラミングにおける抽象化とは |
| 関数・インターフェース・抽象クラスの違い | 手順をまとめる/約束だけを決める/約束と共通の手順を両方持つ | インターフェース・抽象クラス |
| 抽象化すると何が良いのか | 変更が1か所で済む・偽物を差し込んでテストできる | 差分の行数・テストのしやすさ |
| 抽象化しすぎの害 | フラグ引数が増えて組み合わせが爆発する・中身の都合が漏れる | 早すぎる抽象化・漏れのある抽象化 |
| いつ抽象化するか | 同じ形が3回目に出たとき、差し替える理由が見えたとき | 使い分けと判断表 |
| 抽象化思考の鍛え方 | 「つまり何か」と「たとえば何か」を交互に問う | 思考法としての抽象化 |
確認環境:Ubuntu 24.04.5 LTS(2コア)の上で、題材「注文の通知(メール・Slack・SMS)」を Python 3.13.18・OpenJDK 21.0.12.1・TypeScript 5.9.3(作業用のフォルダーに npm で入れた tsc、Node.js 22.23.4 で実行)の3言語で、ベタ書き→関数→インターフェース→抽象クラスの順に書いて実行しました。外部への送信は行わず、送信の部分は画面に出すだけの偽物にしています。Python の型の検査は mypy 2.4.1、変更のしやすさは diff -u の行数、テストは unittest、抽象化のレベルは strace 6.8 のシステムコール、漏れのある抽象化は SQLite 3.53.1(メモリー上のデータベース)と Java の配列で測りました。抽象クラスとインターフェースの決まりは Python・Java・TypeScript の公式ドキュメント、抽象化の定義は MDN、「漏れのある抽象化の法則」は Joel Spolsky 氏、「間違った抽象化」は Sandi Metz 氏の原文で確かめています。図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「抽象化とは?意味とプログラミングの例|3言語で段階実装し差分を比較」
抽象化とは(意味・日常の例・具体化との違い)
この節では、抽象化という言葉のもともとの意味と、日常の例、具体化・捨象・一般化との違いが分かります。
抽象化とは、いくつもの具体的なものから共通する性質を取り出し(一般化)、いまの目的に関係のない性質を捨てる(捨象)ことです。 「抽象的」というと「あいまい」という意味で使われがちですが、抽象化そのものは、目的に合わせて情報を選ぶ、はっきりした作業です。
1路線図
駅の順番と乗り換えだけを残し、線路の曲がり方や本当の距離を捨てている。「どう乗り継ぐか」という目的には、そのほうが読みやすい。
2「乗り物」という言葉
電車・バス・自転車から「人を運ぶ」という共通点だけを取り出した言葉。色や車輪の数は捨てている。
3車の運転
アクセル・ブレーキ・ハンドルだけを操作すれば走れる。エンジンの燃料噴射の仕組みを知らなくても運転できる。
抽象化を作る2つの操作:捨象と一般化
| 言葉 | していること | 例 |
|---|---|---|
| 捨象(しゃしょう) | 目的に関係のない性質を捨てる | 路線図から線路の形を消す |
| 一般化 | 複数のものに共通する性質を取り出して名前を付ける | メール・Slack・SMS をまとめて「通知」と呼ぶ |
| 具体化(具象化) | 抽象から、実際の1つの例や手順に落とす | 「通知する」を「この宛先にこの件名でメールを送る」にする |
| モデル化 | 目的に合わせて、現実を扱える形に写し取る | 注文を「番号・金額・宛先」の3項目で表す |
抽象化と具体化の違い
抽象化(まとめる方向)
「つまり何か」を問う
数が減り、全体が見える
細部が見えなくなる
具体化(細かくする方向)
「たとえば何か」を問う
手を動かせる単位になる
全体の共通点が見えにくくなる
どちらが偉いということはなく、上がったり下がったりを往復するのが抽象化の使い方です。プログラムでも、具体的なコードを何本か書いてから共通点を抜き出し(抽象化)、抜き出した形で新しい処理を書いてみて(具体化)、合わなければ戻す、という往復をします。
プログラミングにおける抽象化とは(定義と2つの種類)
この節では、プログラミングでいう抽象化の定義と、「手続きの抽象化」「データの抽象化」の違いが分かります。
プログラミングにおける抽象化とは、処理やデータの中身を名前の後ろに隠し、使う側が「何をしてくれるか」だけを知っていれば使えるようにすることです。 MDN の用語集は、次のように説明しています。
コンピュータープログラミングにおける抽象化 (abstraction) とは、複雑なソフトウェアシステムにおいて、複雑さを軽減し、また効率的な設計と実装を可能にする方法です。これはシステムの技術的な複雑さを、単純な API の背後に隠します。
| 種類 | 隠すもの | コードでの道具 |
|---|---|---|
| 手続き(制御)の抽象化 | 手順の中身 | 関数・メソッド・ループの構文 |
| データの抽象化 | データの持ち方 | クラス・抽象データ型・モジュール |
| 型の抽象化(約束) | どのクラスが中身を担当するか | インターフェース・抽象クラス・Protocol・トレイト |
| 層の抽象化 | 下の層の仕組み | OS・ドライバー・ORM・HTTP クライアント |
たとえば Python の sorted(data) は、並べ替えの手順(どのアルゴリズムか)を隠した手続きの抽象化です。dict は、ハッシュ表という持ち方を隠したデータの抽象化です。使う側は「キーで値を引ける」という約束だけを知っていれば使えます。
「メソッド」は、クラスに属する関数のことで、抽象化を書く道具の1つです。関数との違いは、関連記事として同時期に公開予定の「メソッドとは?関数との違いとクラスメソッド・staticを実行例で」で扱います。オブジェクト指向全体の考え方は、関連記事「オブジェクト指向とは?コードでわかる基本と使いどころ」で扱っています。
抽象化のレベル(高水準言語・API・OS)をstraceで見る
この節では、ふだん書く1行の下に、どれだけの層の抽象化が重なっているかが、実際のシステムコールで分かります。
Python の print("hi") と open(...).write("abc") は、最終的に OS の write というシステムコール(OS に仕事を頼む入口)になります。 次の3行を strace で見ました。
print("hi")
with open("/srv/s4/r17/t92/py/out.txt", "w") as f:
f.write("abc")
$ strace -e trace=openat,write,close python3 lv.py >/dev/null
openat(AT_FDCWD, "/srv/s4/r17/t92/py/out.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
write(3, "abc", 3) = 3
close(3) = 0
write(1, "hi\n", 3) = 3
openはopenatに、f.writeはwrite(3, ...)に、printはwrite(1, ...)(1は標準出力)になりました。- 先に書いたはずの
print("hi")が、最後に出ています。 出力先がファイルやパイプのとき、Python は標準出力をためてからまとめて書くためです。同じコードを環境変数PYTHONUNBUFFERED=1のまま動かすと、write(1, "hi", 2)とwrite(1, "\n", 1)の2回に分かれて、すぐに出ました。 - 3行のコードを動かすあいだに、Python の起動を含めて456回のシステムコールが呼ばれていました(
PYTHONUNBUFFERED=1の状態でのstrace -cの合計)。
上の層にいると下の層を意識せずに書けます。これが抽象化の効き目です。一方で、出力の順番が入れ替わるように、下の層の都合がときどき表に出ます。これは後の節の「漏れのある抽象化」で扱います。
- 1アプリのコードnotify(order)。業務の言葉で書く
- 2ライブラリ・APIprint・open・HTTP クライアント・ORM
- 3言語の実行環境Python のインタープリター・JVM・Node.js。バッファーやメモリー管理
- 4OS のシステムコールopenat・write・close。ファイルやネットワークを同じ形で扱う
- 5機械語とハードウェアCPU の命令・メモリー・ディスク
同じ題材で段階的に実装する(ベタ書き→関数にまとめる)
この節では、この記事で使う題材と、最初の2段階「ベタ書き」と「関数にまとめる(手続きの抽象化)」のコードと実行結果が分かります。
題材は「注文の受付・発送・取り消しのたびに、メール・Slack・SMS で知らせる」処理です。 実際には送らず、外部サービスの代わりの関数(smtp_send など)が画面に出すだけにしました。
# ext.py:外部サービスの代わり(実際には送らず、画面に出すだけ)
def smtp_send(to, subject, body): print(f"[SMTP] to={to} subject={subject} body={body}")
def slack_post(channel, text): print(f"[Slack] #{channel} {text}")
def sms_send(tel, text): print(f"[SMS] {tel} {text[:70]}")
段階0:ベタ書き(抽象化なし)
from ext import smtp_send, slack_post, sms_send
def complete_order(order):
# 注文完了:3か所に、ほぼ同じ処理をそのまま書いている
msg = f"注文{order['id']}を受け付けました(合計{order['total']:,}円)"
smtp_send(order["email"], "ご注文ありがとうございます", msg)
slack_post("orders", msg)
if order.get("tel"):
sms_send(order["tel"], msg)
def ship_order(order):
msg = f"注文{order['id']}を発送しました"
smtp_send(order["email"], "発送のお知らせ", msg)
slack_post("orders", msg)
if order.get("tel"):
sms_send(order["tel"], msg)
def cancel_order(order): # 同じ形がもう1回(省略)
...
$ python3 s0_concrete.py
[SMTP] to=taro@example.com subject=ご注文ありがとうございます body=注文A-1001を受け付けました(合計12,800円)
[Slack] #orders 注文A-1001を受け付けました(合計12,800円)
[SMS] 090-0000-0000 注文A-1001を受け付けました(合計12,800円)
[SMTP] to=taro@example.com subject=発送のお知らせ body=注文A-1001を発送しました
...(全9行)
動きは正しいのですが、「メール・Slack・SMS に送る」という手順が3回書かれています。送り先が1つ増えると、3か所を直すことになります。
段階1:関数にまとめる(手続きの抽象化)
from ext import smtp_send, slack_post, sms_send
def notify(order, subject, msg):
"""通知の送り方を1か所にまとめた関数(手順の抽象化)"""
smtp_send(order["email"], subject, msg)
slack_post("orders", msg)
if order.get("tel"):
sms_send(order["tel"], msg)
def complete_order(order):
notify(order, "ご注文ありがとうございます",
f"注文{order['id']}を受け付けました(合計{order['total']:,}円)")
def ship_order(order):
notify(order, "発送のお知らせ", f"注文{order['id']}を発送しました")
def cancel_order(order):
notify(order, "取り消しのお知らせ", f"注文{order['id']}を取り消しました")
実行結果は段階0と1文字も変わらず、9行が同じ順番で出ました。呼び出す側(ship_order など)は「通知する」とだけ書き、どこにどう送るかを知らなくてよくなりました。 これが一番小さな抽象化です。行数は27行から22行に減りました。
Java と TypeScript でも、static メソッドや普通の関数にまとめる形は同じです。
static void notify(Map<String, String> order, String subject, String msg) {
System.out.println("[SMTP] to=" + order.get("email") + " subject=" + subject + " body=" + msg);
System.out.println("[Slack] #orders " + msg);
if (order.containsKey("tel")) System.out.println("[SMS] " + order.get("tel") + " " + msg);
}
$ javac -encoding UTF-8 -d out Stage1.java && java -cp out Stage1
[SMTP] to=taro@example.com subject=発送のお知らせ body=注文A-1001を発送しました
[Slack] #orders 注文A-1001を発送しました
[SMS] 090-0000-0000 注文A-1001を発送しました
手元では、最初の実行で日本語が ??????? に化けました。端末の文字コードの設定(LANG)が無かったためで、LANG=C.UTF-8 と -Dstdout.encoding=UTF-8 を付けて直りました。
インターフェースで抽象化する(Python・Java・TypeScript)
この節では、「何ができるか」という約束だけを決めるインターフェースで、送り先そのものを差し替えられるようにする書き方が、3言語の実行結果で分かります。
インターフェースは、「send(order, subject, msg) を持っていれば通知に使える」という約束だけを書き、中身は書かないものです。 段階1の関数は「メール・Slack・SMS の3つに送る」ことが中に固定されていましたが、インターフェースにすると、どこに送るかを外から渡せます。
Python:typing.Protocol
from typing import Protocol
from ext import smtp_send, slack_post, sms_send
class Notifier(Protocol):
"""「通知できるもの」の約束(インターフェース)。中身は書かない"""
def send(self, order: dict, subject: str, msg: str) -> None: ...
class EmailNotifier:
def send(self, order, subject, msg):
smtp_send(order["email"], subject, msg)
class SlackNotifier:
def __init__(self, channel="orders"):
self.channel = channel
def send(self, order, subject, msg):
slack_post(self.channel, msg)
class SmsNotifier:
def send(self, order, subject, msg):
if order.get("tel"):
sms_send(order["tel"], msg)
class OrderService:
def __init__(self, notifiers: list[Notifier]):
self.notifiers = notifiers # 何に送るかは外から渡す
def _notify(self, order, subject, msg):
for n in self.notifiers:
n.send(order, subject, msg)
def ship_order(self, order):
self._notify(order, "発送のお知らせ", f"注文{order['id']}を発送しました")
# complete_order・cancel_order も同じ形
svc = OrderService([EmailNotifier(), SlackNotifier(), SmsNotifier()])
実行結果は段階0・1と同じ9行でした。EmailNotifier などは Notifier を継承していません。Python の Protocol は、形(メソッドの名前と引数)が合っていれば、その型として扱う仕組み(構造的部分型)です。
Java:interface と implements
interface Notifier { // 約束だけを書く
void send(Map<String, String> order, String subject, String msg);
}
class EmailNotifier implements Notifier {
public void send(Map<String, String> order, String subject, String msg) {
System.out.println("[SMTP] to=" + order.get("email") + " subject=" + subject + " body=" + msg);
}
}
class OrderService {
private final List<Notifier> notifiers;
OrderService(List<Notifier> notifiers) { this.notifiers = notifiers; }
void shipOrder(Map<String, String> order) {
for (Notifier n : notifiers) n.send(order, "発送のお知らせ", "注文" + order.get("id") + "を発送しました");
}
}
// メソッドが1つだけのインターフェースなので、ラムダでも渡せる
Notifier fake = (order, subject, msg) -> System.out.println("[Fake] " + msg);
new OrderService(List.of(fake)).shipOrder(o);
$ java -cp out Stage2
[SMTP] to=taro@example.com subject=発送のお知らせ body=注文A-1001を発送しました
[Slack] #orders 注文A-1001を発送しました
[Fake] 注文A-1001を発送しました
TypeScript:interface(JavaScript に変換すると消える)
interface Notifier { // 型としての約束。JS に変換すると消える
send(order: Order, subject: string, msg: string): void;
}
class EmailNotifier implements Notifier {
send(order: Order, subject: string, msg: string) { console.log(`[SMTP] to=${order.email} subject=${subject} body=${msg}`); }
}
class OrderService {
constructor(private notifiers: Notifier[]) {}
shipOrder(order: Order) {
for (const n of this.notifiers) n.send(order, "発送のお知らせ", `注文${order.id}を発送しました`);
}
}
// クラスでなくても、形が合えば渡せる(構造的型付け)
new OrderService([{ send: (_o, _s, msg) => console.log(`[Fake] ${msg}`) }]).shipOrder(o);
tsc で変換した out/stage2.js を見ると、interface の文字は1つも残っていませんでした(grep -c interface の結果が0)。TypeScript のインターフェースはコンパイルのときだけの約束で、実行中には存在しません。 TypeScript の公式ハンドブックも、implements は「クラスがそのインターフェースとして扱えるかの検査にすぎず、クラスやメソッドの型を変えない」と注意しています。
| 言語 | インターフェースの書き方 | 約束を破ったら |
|---|---|---|
| Python | typing.Protocol(継承は不要) |
実行時は何も起きない。mypy などの型チェッカーが指摘 |
| Java | interface と implements |
javac がコンパイルエラー |
| TypeScript | interface(形が合えば implements なしでも可) |
tsc がエラー。JS には残らない |
抽象クラスで抽象化する(共通の手順と差分を分ける)
この節では、約束に加えて共通の手順も持てる抽象クラスで、子クラスに「違う部分だけ」を書かせる方法が、3言語の実行結果で分かります。
抽象クラスは、中身のあるメソッドと、中身の無い抽象メソッドを両方持てるクラスで、そのままでは new(インスタンス化)できません。 「送れるかを確かめ、送れなければ飛ばし、送れるなら送る」という共通の手順を親に書き、「実際にどう送るか」だけを子に書かせます。この形はテンプレートメソッドと呼ばれます。
Python:abc.ABC と @abstractmethod
from abc import ABC, abstractmethod
from ext import smtp_send, slack_post, sms_send
class BaseNotifier(ABC):
"""共通の手順は基底クラスに書き、違う部分だけ子クラスに任せる(抽象クラス)"""
def send(self, order, subject, msg):
if not self.can_send(order):
print(f" skip {type(self).__name__}")
return
self._deliver(order, subject, msg)
def can_send(self, order): # 既定の動き(上書きしてよい)
return True
@abstractmethod
def _deliver(self, order, subject, msg): # 子クラスが必ず書く部分
...
class EmailNotifier(BaseNotifier):
def _deliver(self, order, subject, msg):
smtp_send(order["email"], subject, msg)
class SmsNotifier(BaseNotifier):
def can_send(self, order):
return bool(order.get("tel"))
def _deliver(self, order, subject, msg):
sms_send(order["tel"], msg)
$ python3 s3_abstract.py # 電話番号の無い注文
[SMTP] to=taro@example.com subject=ご注文ありがとうございます body=注文A-1001を受け付けました(合計12,800円)
[Slack] #orders 注文A-1001を受け付けました(合計12,800円)
skip SmsNotifier
Python の公式ドキュメント(abc モジュール)は、ABCMeta から作られたクラスは、すべての抽象メソッドと抽象プロパティが上書きされない限りインスタンス化できない、としています。
Java:abstract class と final
abstract class BaseNotifier {
// 共通の手順(テンプレートメソッド)。final で上書きを禁止
public final void send(Map<String, String> order, String subject, String msg) {
if (!canSend(order)) { System.out.println(" skip " + getClass().getSimpleName()); return; }
deliver(order, subject, msg);
}
protected boolean canSend(Map<String, String> order) { return true; }
protected abstract void deliver(Map<String, String> order, String subject, String msg);
}
class SmsNotifier3 extends BaseNotifier {
@Override protected boolean canSend(Map<String, String> order) { return order.containsKey("tel"); }
protected void deliver(Map<String, String> order, String subject, String msg) {
System.out.println("[SMS] " + order.get("tel") + " " + msg);
}
}
$ java -cp out Stage3
[SMTP] to=taro@example.com subject=発送のお知らせ body=注文A-1001を発送しました
skip SmsNotifier3
Java では send に final を付けると、子クラスが共通の手順そのものを書き換えることを、コンパイルの段階で止められます(エラーの原文は後の節)。
TypeScript:abstract class
abstract class BaseNotifier {
send(order: Order, subject: string, msg: string): void {
if (!this.canSend(order)) { console.log(` skip ${this.constructor.name}`); return; }
this.deliver(order, subject, msg);
}
protected canSend(_order: Order): boolean { return true; }
protected abstract deliver(order: Order, subject: string, msg: string): void;
}
class SmsNotifier extends BaseNotifier {
protected override canSend(order: Order) { return !!order.tel; }
protected deliver(order: Order, _s: string, msg: string) { console.log(`[SMS] ${order.tel} ${msg}`); }
}
$ npx tsc -p . && node out/stage3.js
[SMTP] to=taro@example.com subject=発送のお知らせ body=注文A-1001を発送しました
skip SmsNotifier
変換後の JavaScript では abstract の文字が消え、ふつうの class BaseNotifier になっていました。抽象メソッド deliver の宣言も消え、子クラスの deliver だけが残ります。
ベタ書き
同じ手順を3回書く。27行。変更は3か所。
抽象化なし
関数
手順を notify() にまとめる。22行。送り先は中に固定。
手続きの抽象化
インターフェース
send() の約束だけを決め、送り先を外から渡す。40行。
差し替えられる
抽象クラス
共通の手順を親に、違う部分だけを子に書く。50行。
共通の手順を守らせる
変更のしやすさを差分の行数で比べる
この節では、同じ3つの変更を4段階のコードに加えたときに、何行を、何か所で書き換えたかが、diff の実測で分かります。
抽象化が進むほど、どんな変更でも楽になるわけではありません。 変更の種類によって、得をする段階が違いました。diff -u の「+」と「−」の行を数えた結果です(Python)。
| 変更 | 段階0 ベタ書き | 段階1 関数 | 段階2 インターフェース | 段階3 抽象クラス |
|---|---|---|---|---|
| A:Teams にも送る | +4 −1(3か所) | +2 −1(1か所) | +6 −2(新しいクラス+登録) | +6 −2(新しいクラス+登録) |
| B:送るたびに送信ログを1行残す | +9 −0(9か所) | +3 −0(3か所) | +1 −0(1か所) | +1 −0(1か所) |
| C:メールだけ送る窓口を足す | +9 −6(既存の if を全部直す) | +3 −2(notify の中を直す) | +2 −0(呼び出しを足すだけ) | +2 −0(呼び出しを足すだけ) |
- 変更A(送り先を1つ足す)は、関数の段階が一番小さい差分でした。 ただし、
notifyという既存の関数の中身を書き換えています。段階2・3は差分が6行と多めですが、既存のクラスには触れず、新しいクラスを足して登録する1行を直しただけでした。 - 変更C(客によって送り先を変える)は、段階0・1では既存の if を書き換え、段階2・3では既存のコードに触れずに済みました。 「何に送るか」を外から渡せる形にしたことが効いています。
- 段階2と3の差が出なかったのは、この題材の共通の手順が短いからです。共通の手順(再送・ログ・宛先の確認)が増えるほど、抽象クラスに置く意味が大きくなります。
「既存のコードを書き換えずに、足すだけで機能を増やせる」という性質は、SOLID 原則の開放閉鎖の原則(OCP)と呼ばれます。5つの原則は、関連記事「SOLID原則をわかりやすく|早見表とJavaの直す前・後で比較」で扱っています。
テストのしやすさを比べる(偽物を差し込めるか)
この節では、抽象化の段階によって、外部に送らずにテストを書く手間がどう変わるかが、実際のテストコードと実行結果で分かります。
インターフェースの段階では、約束(send)を守った偽物を渡すだけでテストが書けました。 関数の段階では、中で呼んでいる3つの関数をすべて差し替える(モックする)必要があります。
段階1(関数)のテスト:3つを差し替える
import unittest
from unittest import mock
import s1_function as m
class T(unittest.TestCase):
def test_complete_sends_three(self):
with mock.patch.object(m, "smtp_send") as smtp, \
mock.patch.object(m, "slack_post") as slack, \
mock.patch.object(m, "sms_send") as sms:
m.complete_order({"id": "A-1", "total": 100, "email": "a@example.com", "tel": "090"})
smtp.assert_called_once()
slack.assert_called_once()
sms.assert_called_once()
self.assertIn("A-1", slack.call_args.args[1])
段階2(インターフェース)のテスト:偽物を渡す
import unittest
from s2_interface import OrderService
class FakeNotifier: # 約束(send)さえ守れば何でも差し込める
def __init__(self): self.sent = []
def send(self, order, subject, msg): self.sent.append(msg)
class T(unittest.TestCase):
def test_complete_sends_message(self):
fake = FakeNotifier()
OrderService([fake]).complete_order({"id": "A-1", "total": 100})
self.assertEqual(fake.sent, ["注文A-1を受け付けました(合計100円)"])
どちらも Ran 1 test ... OK でした。ところが、段階1のテストで sms_send の差し替えを1つ書き忘れると、次のようになりました。
$ python3 test_s1_forgot.py
[SMS] 090 注文A-1を受け付けました(合計100円)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
テストは OK のまま、SMS の送信処理が本当に呼ばれていました。 この題材では画面に出るだけですが、本物の送信処理なら、テストのたびに SMS が送られます。インターフェースの段階では、テスト用の OrderService に本物の送り先を渡さない限り、本物は呼ばれません。
| 比べたこと | 段階1(関数) | 段階2(インターフェース) |
|---|---|---|
| テストの行数(空行を除く) | 15行 | 12行 |
| 差し替えるもの | モジュールの関数3つ(名前を文字列で指定) | 偽物のオブジェクトを1つ渡す |
| 差し替えを忘れたとき | 本物が呼ばれ、テストは OK のまま | 偽物しか渡していないので本物は呼ばれない |
| 関数名を変えたとき | 文字列の "sms_send" も直す(直し忘れはテスト時のエラーで分かる) | 約束の名前を変えれば型チェッカーが指摘 |
Java でも、Notifier はメソッドが1つのインターフェースなので、(order, subject, msg) -> ... というラムダを偽物として渡せました(前の節の [Fake] の行)。
インターフェースと抽象クラスの使い分け(判断表)
この節では、関数・インターフェース・抽象クラスのどれを選ぶか、抽象化とカプセル化・継承・ポリモーフィズムがどう違うかが、表で分かります。
迷ったら「関数」から始め、差し替える理由が出たらインターフェース、共通の手順を守らせたくなったら抽象クラス、の順に進めます。 Java の公式チュートリアルは、近い関係のクラスでコードを共有したいとき、protected などの修飾子やフィールドを持たせたいときは抽象クラスを、関係の無いクラスに同じ振る舞いを持たせたいときや型の多重継承をしたいときはインターフェースを検討する、としています。
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 同じ手順を2〜3か所に書いている | 関数 | いちばん小さい抽象化で足りる |
| 中身を差し替えたい(本番と偽物・送り先の追加) | インターフェース | 約束だけを決め、実装を外から渡せる |
| 関係の無いクラスに同じ能力を持たせたい | インターフェース | 複数実装できる(Java は extends が1つだけ) |
| 共通の手順を子クラスに守らせたい | 抽象クラス | 手順を親に書き、違う部分だけを抽象メソッドにする |
| 共通のフィールド(状態)を持たせたい | 抽象クラス | インターフェースは状態を持てない(Java) |
| 実装が1つしか無く、増える見込みも無い | 抽象化しない(具体クラスのまま) | 読む場所が増えるだけ |
| Rust・PHP・Scala で振る舞いを共有したい | トレイト | インターフェースに近い。言語ごとに意味が違う |
Rust・PHP のトレイトは、同時期に公開予定の「trait(トレイト)とは|Rust・PHPの使い方・違い表・エラー原文19種」で扱います。言語ごとの細かい違いは、関連記事「Javaインターフェースと抽象クラスの違い|比較表と使い分け」、関連記事「Python 抽象クラスとProtocol|ABCとの違い・使い分け早見表」、関連記事「TypeScript abstract classとinterfaceの違いと例」で扱っています。
抽象化とカプセル化・継承・ポリモーフィズムの違い
| 言葉 | 何をすることか | この記事の題材では |
|---|---|---|
| 抽象化 | 共通点を取り出し、使う側に見せるものを決める | 「通知できるもの」という約束を決めた |
| カプセル化 | 中のデータや手順を外から触れないように閉じる | SlackNotifier のチャンネル名を中に持たせた |
| 継承 | 親クラスの中身を子クラスが受け継ぐ | EmailNotifier が BaseNotifier の send を受け継いだ |
| ポリモーフィズム(多態性) | 同じ呼び出しで、相手によって違う動きをする | n.send(...) がメール・Slack・SMS で違う動きをした |
抽象化は「何を見せるか」を決める考え方、カプセル化は「見せないものを閉じる」仕組み、ポリモーフィズムは「決めた約束を通して、違う中身を同じように呼べる」効果、と分けると整理しやすくなります。
悪い抽象化①:早すぎる抽象化とフラグ引数
この節では、似ているからと早くまとめた関数が、要望のたびに引数と if を増やして読めなくなる様子が、測った数値で分かります。
まだ違いが見えていないうちにまとめた抽象化は、要望が増えるたびにフラグ引数(True/False の引数)と分岐が増えていきます。 Sandi Metz 氏は、この過程を説明したうえで「duplication is far cheaper than the wrong abstraction(重複は、間違った抽象化よりはるかに安い)」と書いています。
通知の本文を作る関数を、最初は2種類の文面だけでまとめました(v1)。その後「SMS は短く」「英語版も」「法人には請求先を」「HTML メールも」と4つの要望を同じ関数に足した版(v4)です。
def build_message(kind, order, sms=False, english=False, corporate=False, html=False):
if english:
text = f"Order {order['id']} " + ("received" if kind == "complete" else "shipped")
elif kind == "complete":
text = f"注文{order['id']}を受け付けました"
if corporate:
text += f"(請求先:{order['company']})"
else:
text = f"注文{order['id']}を発送しました"
if corporate and not sms:
text += "(納品書を同封しています)"
if sms:
text = text[:70]
elif html:
text = f"<p>{text}</p>"
return text
Python の ast モジュールで、引数・分岐・真偽フラグの数を数えました。
$ python3 measure.py v1.py v4.py split.py
v1.py 関数 1 最大引数 2 分岐 1 真偽フラグ 0 組み合わせ 2 行 4
v4.py 関数 1 最大引数 6 分岐 7 真偽フラグ 4 組み合わせ 32 行 16
split.py 関数 7 最大引数 2 分岐 1 真偽フラグ 0 組み合わせ 2 行 8
真偽フラグが4つと種類が2つで、呼び方の組み合わせは 2×2×2×2×2=32通りになりました。さらに、組み合わせによっては指定が黙って無視されました。
>>> build_message('complete', o, corporate=True, sms=True, html=True)
'注文A-1を受け付けました(請求先:ABC商事)' # html=True が効いていない
>>> build_message('shipped', o, english=True, corporate=True)
'Order A-1 shipped' # corporate=True が効いていない
エラーにならずに指定が無視されるので、気づくのは本番の文面を見たときです。 使う側ごとに小さな関数へ分け直した版(split.py)は、関数の数は7つに増えましたが、1つずつの引数は2つ以下、分岐は全体で1つになりました。
似た処理が2つ出てきたとき
見分ける問い:「この引数は、呼ぶ人によって意味が変わっていないか」
まとめた関数に合わない要望が来たとき
Sandi Metz 氏が勧める戻し方
悪い抽象化②:漏れのある抽象化(中身の都合が表に出る)
この節では、隠したはずの下の層の都合が、速さや結果に表れる「漏れのある抽象化」を、4つの実測で確かめた結果が分かります。
Joel Spolsky 氏は「All non-trivial abstractions, to some degree, are leaky.(自明でない抽象化は、どれもある程度は漏れる)」と書き、これを「漏れのある抽象化の法則」と呼びました。 抽象化は細部を隠しますが、細部を消すわけではありません。手元で次の4つを確かめました。
1. 「客を1人取ってくる」が SQL を5,001回流す(N+1)
class CustomerRepository:
"""「客を1人取ってくる」だけの抽象化。中で SQL が走ることは見えない"""
def get(self, cid):
return db.execute("SELECT name FROM customers WHERE id=?", (cid,)).fetchone()[0]
rows = db.execute("SELECT id, customer_id FROM orders").fetchall() # 注文5,000件
names = [repo.get(cid) for _, cid in rows] # 1件ずつ取る
$ python3 leak_repo.py
抽象化のまま : SQL 5001 回 15.4 ms
JOIN 1回 : SQL 1 回 6.1 ms
同じ結果: True
SQLite の set_trace_callback で数えると、repo.get を5,000回呼んだぶん、SQL が5,001回流れていました。JOIN を使った1回の SQL と結果は同じです。今回はメモリー上の SQLite なので差は2.5倍でしたが、ネットワークの向こうのデータベースなら、1回ごとに往復の待ち時間が加わります。
2. 同じ合計なのに、配列を読む順番で速さが変わる(Java)
int n = 4096;
int[][] a = new int[n][n];
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) s1 += a[i][j]; // 行ごと
for (int j = 0; j < n; j++) for (int i = 0; i < n; i++) s2 += a[i][j]; // 列ごと
round 0 行ごと 128 ms 列ごと 459 ms (sum 0 0)
round 1 行ごと 35 ms 列ごと 314 ms (sum 0 0)
round 2 行ごと 12 ms 列ごと 235 ms (sum 0 0)
round 3 行ごと 9 ms 列ごと 326 ms (sum 0 0)
round 4 行ごと 6 ms 列ごと 405 ms (sum 0 0)
「2次元の配列」という抽象化の上では、どちらも同じ1,600万個の足し算です。実際には、メモリー上に並んだ順に読む「行ごと」が、5回目で 6 ms、「列ごと」が 405 ms と、60倍以上の差になりました。CPU のキャッシュという下の層の都合が漏れています。
3. 小数の足し算が 0.3 にならない
$ python3 -c "print(0.1+0.2, 0.1+0.2==0.3)"
0.30000000000000004 False
$ node -e "console.log(0.1+0.2)"
0.30000000000000004
「数」という抽象化の下で、2進数の浮動小数点(IEEE 754)という持ち方が漏れています。金額の計算では、Python の decimal や Java の BigDecimal、または「円」の整数で持つ方法を使います。
4. print の順番が入れ替わる
strace の節で見たとおり、print を先に書いても、出力先がファイルのときは後から出ました。ログを別のファイルに書く処理と混ぜると、順番が実際の出来事と食い違うことがあります。
- 1ループの中で、データベース・ネットワークを呼ぶ抽象化を呼んでいないか呼び出し回数をログや trace で数える。まとめて取る API を用意する
- 2遅いときは、1つ下の層を見るSQL のログ・strace・プロファイラー。抽象化の上だけを見ていても原因は見えない
- 3お金・個数は浮動小数点で持たない整数か decimal・BigDecimal を使う
- 4自分の抽象化の「漏れる点」を書いておく「1回の呼び出しで SQL が1本走る」などを docstring やコメントに残す
抽象化の実行時のコスト
関数やメソッドを1段はさむぶんの時間も測りました(Python、timeit で100万回×5回の最小値)。
ベタ書き x+1 28.3 ns/回
関数 direct(x) 56.1 ns/回
メソッド p.run(x) 69.1 ns/回
抽象クラス経由 c.run(x) 79.8 ns/回
1段ごとに数十ナノ秒(1秒の1億分の数秒)増えました。1回の SQL や通信に比べると桁違いに小さく、ふつうの業務のコードでは、抽象化の段数より、漏れ(N+1 など)のほうが速さに効きます。 1秒に何億回も回る計算の中だけは、段数を気にします。
抽象化のよくあるエラーとつまずき(原文つき)
この節では、インターフェース・抽象クラスを書いたときに出るエラーの原文と、原因・直し方が3言語分まとめて分かります。
| 言語 | エラーの原文 | 原因と直し方 |
|---|---|---|
| Python | TypeError: Can't instantiate abstract class BaseNotifier without an implementation for abstract method '_deliver' |
抽象クラスそのものを作った。子クラスを作る |
| Python | TypeError: Can't instantiate abstract class LineNotifier without an implementation for abstract method '_deliver' |
子クラスで名前を deliver と綴り違えた。抽象メソッドと同じ名前にする |
| Python(mypy) | error: List item 1 has incompatible type "FaxNotifier"; expected "Notifier" [list-item] |
Protocol の send の引数が足りない。mypy の note に Expected と Got が並ぶ |
| Python(実行時) | TypeError: FaxNotifier.send() takes 3 positional arguments but 4 were given |
Protocol は実行時に検査されない。呼んだ時点で初めて落ちる |
| Java | error: LineNotifier is not abstract and does not override abstract method deliver(Map<String,String>,String,String) in BaseNotifier |
抽象メソッドを書いていない(綴り違いを含む)。@Override を付けると、綴り違いの行で method does not override or implement a method from a supertype が出て気づける |
| Java | error: BaseNotifier is abstract; cannot be instantiated |
抽象クラスを new した。子クラスか、無名クラスで中身を書く |
| Java | error: FaxNotifier is not abstract and does not override abstract method send(Map<String,String>,String,String) in Notifier |
interface のメソッドと引数が合っていない |
| Java | error: send(Map<String,String>,String,String) in BadSms cannot override send(Map<String,String>,String,String) in BaseNotifier / overridden method is final |
final を付けた共通の手順を上書きしようとした。違う部分は抽象メソッドに書く |
| TypeScript | error TS2515: Non-abstract class 'LineNotifier' does not implement inherited abstract member deliver from class 'BaseNotifier'. |
抽象メンバーを書いていない |
| TypeScript | error TS2511: Cannot create an instance of an abstract class. |
抽象クラスを new した |
| TypeScript | error TS2420: Class 'FaxNotifier' incorrectly implements interface 'Notifier'. / Property 'close' is missing ... |
interface のメソッドが足りない |
| TypeScript | error TS2416: Property 'send' in type 'FaxNotifier' is not assignable to the same property in base type 'Notifier'. |
引数の型が違う(string と number) |
Python の Protocol は、実行するまで約束違反が分からない
引数を1つ忘れた FaxNotifier を混ぜて動かすと、mypy は事前に止めてくれましたが、そのまま実行した場合は次のようになりました。
[SMTP] to=a@example.com subject=ご注文ありがとうございます body=注文A-1を受け付けました(合計1円)
Traceback (most recent call last):
...
File "/srv/s4/r17/t92/py/s2_interface.py", line 28, in _notify
n.send(order, subject, msg)
TypeError: FaxNotifier.send() takes 3 positional arguments but 4 were given
メールは送ったあとで落ちています。 途中まで送って止まるので、送った・送っていないが混ざります。@runtime_checkable を付けた Protocol で isinstance(FaxNotifier(), Notifier) を調べても、結果は True でした。実行時の検査は「send という名前があるか」だけで、引数の数までは見ません。Python で Protocol を使うなら、mypy や pyright などの型チェッカーを CI で動かします。
TypeScript の abstract は、JavaScript では消える
// @ts-ignore で型チェックを黙らせて抽象クラスを new し、変換後の JavaScript を動かしました。
TypeError: this.deliver is not a function
at BaseNotifier.send (/srv/s4/r17/t92/ts/err/out/e2.js:12:61)
tsc はエラーを出しても既定では JavaScript を出力するため、実行はできてしまいます。noEmitOnError を有効にすると、エラーがあるときは出力しません。
思考法としての抽象化(要件整理と、具体と抽象の往復)
この節では、コードを書く前の要件整理で抽象化をどう使うかと、抽象化の力の鍛え方が分かります。
要件整理での抽象化は、ばらばらの要望から「本当に欲しいもの」を1つの言葉にまとめることです。 今回の題材も、最初は別々の要望でした。
| 具体的な要望 | つまり何か(抽象化) | たとえば(具体化して確かめる) |
|---|---|---|
| 注文が入ったらメールがほしい(お客さま) | 出来事が起きたら、関係者に知らせたい | 発送・取り消しでも同じか?→同じ |
| 注文を Slack で見たい(営業) | 法人の客にも Slack に流すか?→流す | |
| SMS もほしい(お客さま) | 電話番号が無い客は?→送らない |
「知らせたい」とまとめると、「送り先は増える・客によって変わる」という次の要望が予想できます。これが、段階2で送り先を外から渡す形にした理由です。逆に「たとえば」で具体化して確かめないと、「電話番号が無い客」のような例外を見落とします。
具体例を3つ以上並べる
要望・コード・不具合の例を、まず具体的なまま書き出す。1つだけでは共通点が分からない。
「つまり何か」と問う
共通点を一言にする。目的に関係のない違い(色・言語・送り先の名前)は捨てる。
「たとえば何か」と問い直す
まとめた言葉から、まだ出ていない具体例を作ってみる。当てはまらない例が出たら、まとめ方を見直す。
名前を付ける
「通知」「送り先」のように、チームで同じ意味で使える名前にする。名前が付かないまとめ方は、たいてい無理がある。
合わなくなったら戻す
新しい具体例が合わなくなったら、まとめ方を作り直す。コードでいう、早すぎる抽象化の戻し方と同じ。
- 抽象化しすぎると、話が伝わらなくなります。 「顧客体験を良くする」まで上げると、何を作ればよいかが決まりません。いまの目的で手を動かせる高さで止めます。
- 人に説明するときは、抽象(つまり)と具体(たとえば)を1つずつ組にして話すと、認識のずれが減ります。
よくある質問
Q. 抽象化とは、簡単に言うと何ですか? A. 共通する性質だけを取り出し、いまの目的に関係のない細部を捨てて扱いやすくすることです。路線図が、駅の順番と乗り換えだけを残して線路の形を捨てているのが身近な例です。
Q. 抽象化と具体化の違いは何ですか? A. 抽象化は「つまり何か」とまとめる方向、具体化は「たとえば何か」と細かくする方向です。どちらか一方ではなく、往復して使います。
Q. 抽象化と捨象の違いは何ですか? A. 捨象は「関係のない性質を捨てる」操作で、抽象化を作る手順の1つです。抽象化は、捨象と一般化(共通点を取り出して名前を付ける)を合わせたものです。
Q. プログラミングで、抽象化の一番簡単な例は何ですか?
A. 関数です。同じ手順を関数にまとめると、呼ぶ側は関数名と引数だけを知っていれば使えます。Python の print や sorted も、中身を隠した抽象化です。
Q. インターフェースと抽象クラスは、どちらを使えばよいですか? A. 中身を差し替えたいだけならインターフェース、共通の手順や状態を子クラスに引き継がせたいなら抽象クラスです。Java ではクラスは1つしか継承できないため、迷ったらインターフェースから始めます。
Q. 抽象化とカプセル化はどう違いますか? A. 抽象化は「使う側に何を見せるか」を決める考え方、カプセル化は「見せないものを外から触れないように閉じる」仕組みです。抽象化で決めた約束を、カプセル化で守る関係です。
Q. 抽象化はいつすればよいですか? A. 同じ形が3回目に出たとき、または中身を差し替える理由(テストの偽物・送り先の追加など)が見えたときです。1回目・2回目は重複のまま置き、違いを見てからまとめるほうが、早すぎる抽象化を防げます。
Q. 抽象化しすぎると何が困りますか? A. フラグ引数と分岐が増えて読めなくなる、処理を追うために開くファイルが増える、下の層の都合(SQL の回数など)が見えなくなる、の3つです。この記事の実測では、4つのフラグで呼び方が32通りになり、指定が黙って無視される組み合わせがありました。
Q. 抽象化すると処理は遅くなりますか? A. 手元の Python では、関数やメソッドを1段はさむごとに数十ナノ秒増えました。SQL や通信1回に比べればごく小さく、ふつうは抽象化の段数より、ループの中で SQL を呼ぶといった「漏れ」のほうが速さに効きます。
Q. Python のインターフェースは何で書きますか?
A. typing.Protocol(形が合えばよい)か、abc.ABC と @abstractmethod(継承させる)です。Protocol は実行時には引数の数まで検査しないため、mypy などの型チェッカーと一緒に使います。
Q. 抽象化思考はどう鍛えますか? A. 具体例を3つ以上並べて「つまり何か」と一言にし、その言葉から「たとえば」と新しい例を作って当てはまるかを確かめる、という往復を繰り返します。コードでも、似た処理を3つ書いてから共通点を抜き出す練習が効きます。
まとめ
- 抽象化とは、共通点を取り出し(一般化)、目的に関係のない細部を捨てる(捨象)こと。具体化と往復して使う。
- プログラミングでは、関数(手順を隠す)→インターフェース(約束だけを決める)→抽象クラス(約束と共通の手順を持つ)の順に、隠す範囲が広がる。3言語とも同じ形で書けたが、Protocol と TypeScript の interface は実行時には検査されない。
- 差分の実測では、送り先を1つ足す変更は関数が最小(2行)、送信ログを足す変更はベタ書きで9か所・インターフェースで1か所、客ごとに送り先を変える変更はインターフェースなら既存のコードを0行の書き換えで済んだ。
- インターフェースにすると偽物を渡すだけでテストが書け、モックの差し替え忘れで本物が呼ばれる事故を防げた。
- 早すぎる抽象化はフラグ引数を増やし(4つで32通り、指定が黙って無視される組み合わせあり)、漏れのある抽象化は SQL を5,001回流し、配列の読む順番で60倍以上の差を生んだ。抽象化は「3回目」と「差し替える理由」が見えてから行う。
参考・出典
確認日はいずれも 2026年10月29日です。
- MDN「Abstraction(抽象化)」:https://developer.mozilla.org/ja/docs/Glossary/abstraction
- Python ドキュメント「abc --- 抽象基底クラス」:https://docs.python.org/3/library/abc.html
- Python ドキュメント「typing --- 型ヒントのサポート」(Protocol・runtime_checkable):https://docs.python.org/3/library/typing.html
- Oracle「The Java Tutorials: Abstract Methods and Classes」:https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html
- TypeScript「Handbook: Classes」(abstract・implements):https://www.typescriptlang.org/docs/handbook/2/classes.html
- Joel Spolsky「The Law of Leaky Abstractions」(2002年11月11日):https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/
- Sandi Metz「The Wrong Abstraction」(2016年1月20日):https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
- mypy ドキュメント「Protocols and structural subtyping」:https://mypy.readthedocs.io/en/stable/protocols.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「抽象化とは?意味とプログラミングの例|3言語で段階実装し差分を比較」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。