プログラミングを学び始めると、文法やライブラリの知識に目が行きがちですが、上達のカギを握るのは「抽象化」という思考そのものです。
抽象化は、複雑な問題を整理し、読みやすく保守しやすいコードへと導く、エンジニアにとっての最強の武器と言っても過言ではありません。
本記事では、日常生活の例からスタートし、実際のコードレベルまで落とし込みながら、プログラミングにおける抽象化をわかりやすく解説していきます。
抽象化とは何か
抽象化の意味と日常生活の例
抽象化とは、対象から細かい違いや余計な情報を取り除き、本質的な共通点だけを取り出して扱いやすくすることを指します。
プログラミングに限らず、私たちは日常的に抽象化を使っています。
たとえば、コンビニで「ペットボトルのお茶を買ってきて」と頼む場面を考えてみます。
このとき本当は「500mlで、緑茶で、砂糖なしで、特定のメーカーで…」と条件がいくつもありますが、会話の中ではそこまで細かく指定しません。
ここでは「ペットボトル」「お茶」という抽象的な概念を使い、細部を省略してコミュニケーションしています。
スマホも同じです。
私たちは「スマホを使う」と言うとき、内部でどのCPUが動いているか、OSがどのようにメモリ管理しているかなどはほとんど意識しません。
「電話ができる」「アプリが動く」「写真が撮れる」といった機能だけを捉えて、細部を気にしない。
これも立派な抽象化です。

抽象化のポイントは、「すべてを詳しく扱わない代わりに、扱いやすくする」ことにあります。
プログラミングでは、この考え方をコードの構造や設計に応用していきます。
抽象化と具体化の違い
抽象化と対になる概念が具体化です。
両者の関係を理解すると、どのレベルで考えるべきかが見えやすくなります。
抽象化では「共通点をまとめる」「本質だけ取り出す」方向に思考を進めます。
一方、具体化では「詳細を決める」「実際の手順や値を詰めていく」方向に進みます。
| 視点 | 抽象化 | 具体化 |
|---|---|---|
| 日常の例 | 「飲み物」 | 「500mlのペットボトルの緑茶」 |
| 要件定義 | 「ユーザーが情報を閲覧できる」 | 「ログイン後に記事一覧APIを呼び、10件ずつ表示する」 |
| プログラム | 「検索処理」 | 「MySQLでLIKE句を使ってnameカラムを検索する」 |
良い設計は、抽象化と具体化を行き来しながらレベルを調整していく作業とも言えます。
抽象化しすぎると「結局何をするのか分からない」状態になり、具体化しすぎると「少し条件が変わるだけで全部作り直しになる」硬い設計になってしまいます。
なぜ初心者ほど抽象化を意識すべきか
初心者のうちは「とりあえず動けばOK」という発想になりがちです。
その結果、以下のような問題が起こりやすくなります。
- 同じような処理を何度もコピペしてしまい、コードが散らかる
- 条件分岐が増えすぎて
ifだらけの「スパゲッティコード」になる - 機能追加のたびに、あちこちを直さないといけなくなる
これらは全て、うまく抽象化できていないことの副作用です。
逆に言えば、初心者の段階から「この処理はまとめられないか」「この2つの違いと共通点は何か」と意識しておくだけで、成長スピードは大きく変わります。

抽象化は高度な理論ではなく、「似たものをまとめる」「余計な情報は見ない」といった、誰でも練習できる思考スキルです。
意識し始めた瞬間から、コードの書き方が変わっていきます。
プログラミングにおける抽象化の考え方
プログラミングでの抽象化は、大きく分けて次の3つの方向から捉えると理解しやすくなります。
- データの抽象化
- 振る舞い(処理)の抽象化
- レベル分けによる抽象化
さらに、これらはカプセル化や情報隠蔽と深く関係しています。
データの抽象化
データの抽象化とは、現実世界のものごとを、扱いやすいデータ構造として表現し直すことです。
たとえば「ユーザー」という概念をプログラムで扱う場合、現実にはさまざまな情報があり得ます。
- 氏名
- メールアドレス
- 住所
- 生年月日
- 購入履歴
- お気に入り商品
- 通知の配信設定 など
しかし、ログイン機能だけを考えれば、本当に必要なのは「識別子」と「認証情報」くらいです。
このとき、以下のような形でデータを抽象化できます。
type User = {
id: string;
email: string;
passwordHash: string;
};
現実のユーザーから見ればかなり情報を削っていますが、「ログインさせる」という目的に絞れば十分な抽象化になっています。
目的に応じて、どこまで情報を持たせるかを調整することがデータの抽象化です。
振る舞いの抽象化
振る舞いの抽象化とは、「何をするか」は決めるが、「どう実現するか」は隠すことです。
たとえばsendEmail(to, subject, body)という関数があったとします。
この関数を使う側は、「メールを送る」という振る舞いだけを意識すればよく、SMTPサーバの設定やリトライ処理などの詳細を知る必要はありません。

ここで大事なのは、「何をしてほしいか」という依頼の形(インターフェース)を整理すること自体が抽象化だという点です。
実装の細かい部分は後から変わっても、依頼の形が安定していれば、コードの多くは影響を受けずに済みます。
レベル分けの抽象化
レベル分けによる抽象化は、大きな問題を階層に分解し、それぞれのレベルで扱うべき関心ごとを分ける考え方です。
Webアプリを例にすると、ざっくりと次のようにレベルを分けられます。
- UIレベル(画面の見た目と操作)
- アプリケーションロジックレベル(ビジネスルール、処理の流れ)
- データアクセスレベル(DBとのやり取り)
UIレベルでは「どのボタンを押したら何が起こるか」を考え、データアクセスレベルでは「SQLをどう書くか」を考えます。
各レベルが自分の責任範囲だけに集中できるように分離・整理することがレベル分けによる抽象化です。
このレベル分けが甘いと、UIのコードの中にSQLが直接書かれたりして、修正するたびに影響範囲が読めなくなってしまいます。
抽象化とカプセル化・隠蔽との関係
オブジェクト指向でよく出てくるカプセル化や情報隠蔽は、抽象化とセットで理解するとスッキリします。
- 抽象化: 何をできるように見せるかを決める(インターフェースやAPIの設計)
- カプセル化: 関連するデータと振る舞いを1つのまとまり(クラスなど)に閉じ込める
- 情報隠蔽: 外から見せるべきでない内部の詳細を隠す
抽象化で「見せる顔」を決め、カプセル化と隠蔽で「裏側の複雑さ」を閉じ込める、という関係になっています。
これらを意識して設計することで、「外からは簡単に扱えるが、中では必要な複雑さを抱え込んでいる」良いモジュールが作れるようになります。
コードで学ぶ抽象化の具体例
ここからは、実際のコード例を通して、抽象化がどのように現れるのかを見ていきます。
言語はイメージしやすいようにJavaScript/TypeScript風の疑似コードで示しますが、考え方は他の言語でも共通です。
関数による抽象化
関数は、最も身近で基本的な抽象化のツールです。
次のようなコードを考えてみます。
// 抽象化されていない例
const price = 1000;
const taxRate = 0.1;
const totalPrice = Math.floor(price * (1 + taxRate));
console.log(totalPrice);
この計算があちこちに散らばると、税率が変わったり計算方法を変えたいときに、すべての箇所を探して直さなければなりません。
そこで、「税込価格を計算する」という振る舞いを1つの関数にまとめると、次のようになります。
function calcTaxIncluded(price, taxRate) {
return Math.floor(price * (1 + taxRate));
}
const price = 1000;
const totalPrice = calcTaxIncluded(price, 0.1);
console.log(totalPrice);
ここではcalcTaxIncludedという名前が「何をしたいのか」という抽象的な意図を表しており、関数の中身は「どうやってやるか」という具体的な実装です。
呼び出し側は、計算の詳細ではなく「税込価格が欲しい」という意図だけに集中できます。
クラスとインターフェースによる抽象化
オブジェクト指向言語では、クラスやインターフェースを使うことで、より大きな単位で抽象化できます。
たとえば、ログ出力を行う処理を考えてみます。
// 抽象インターフェース
interface Logger {
logInfo(message: string): void;
logError(message: string): void;
}
// 実装1: コンソールに出力
class ConsoleLogger implements Logger {
logInfo(message: string) {
console.log("[INFO]", message);
}
logError(message: string) {
console.error("[ERROR]", message);
}
}
// 実装2: ファイルに出力(イメージ)
class FileLogger implements Logger {
logInfo(message: string) {
// ファイルに書き込む処理…
}
logError(message: string) {
// ファイルに書き込む処理…
}
}
アプリケーションの本体側では、Loggerインターフェースだけを意識して使うようにします。
function doSomething(logger: Logger) {
logger.logInfo("処理を開始します");
// 何らかの処理
logger.logInfo("処理が完了しました");
}
このようにしておくと、ログの出力方法を変えたくなったときに、doSomething側のコードを一切変えずに差し替えられるようになります。
インターフェースは、「ログを出す」という行為の抽象化であり、具体的にどこにどう出すかは実装クラスに任せています。

ライブラリ・フレームワークが提供する抽象化
普段から使っているライブラリやフレームワークも、抽象化のかたまりです。
たとえば、HTTPリクエストを送る処理を、低レベルから考えると次のような詳細があります。
- TCP接続の確立
- HTTPヘッダの構成
- リクエストボディのエンコード
- レスポンスのステータスコード解析
- エラー時のリトライやタイムアウト処理 など
しかし、多くの言語ではライブラリのおかげで、次のような1行のコードで済ませられます。
const res = await fetch("https://api.example.com/users");
fetchという関数が、HTTP通信という複雑な処理を「URLに対してリクエストを送ってレスポンスを返す」という単純なインターフェースに抽象化してくれているわけです。
フレームワークも同様で、ルーティング、テンプレート処理、DBアクセスなどをうまく抽象化することで、開発者がビジネスロジックに集中できるようにしてくれます。
条件分岐の整理に役立つ抽象化
抽象化は、条件分岐の整理にも大きく貢献します。
たとえば、ユーザー種別ごとに料金計算をしているようなコードを考えます。
// よくあるアンチパターン
function calcPrice(userType, basePrice) {
if (userType === "guest") {
return basePrice;
} else if (userType === "member") {
return basePrice * 0.9;
} else if (userType === "vip") {
return basePrice * 0.8;
} else {
throw new Error("unknown user type");
}
}
ユーザー種別が増えるたびにifが増えていき、分岐の塊になります。
ここで、「割引率」という概念で抽象化してみます。
const discountRateByUserType = {
guest: 1.0,
member: 0.9,
vip: 0.8,
};
function calcPrice(userType, basePrice) {
const rate = discountRateByUserType[userType];
if (rate == null) {
throw new Error("unknown user type");
}
return basePrice * rate;
}
さらに抽象化を進めて、ユーザー自体が「自分の割引率」を知るようにしてもよいでしょう。
interface User {
getDiscountRate(): number;
}
class GuestUser implements User {
getDiscountRate() { return 1.0; }
}
class MemberUser implements User {
getDiscountRate() { return 0.9; }
}
class VipUser implements User {
getDiscountRate() { return 0.8; }
}
function calcPrice(user: User, basePrice: number) {
return basePrice * user.getDiscountRate();
}
ここまでくると、条件分岐は「ユーザーの種類ごとの実装」という形に吸収され、料金計算ロジックからは消えていることが分かります。
これもまた、抽象化の力です。
抽象化を使いこなすためのステップ
抽象化は一朝一夕で身につくものではありませんが、意識的に練習することで確実に鍛えられるスキルです。
この章では、失敗パターンから良い抽象化の観点、レベルの調整方法、練習方法まで順に見ていきます。
初心者がやりがちな抽象化の失敗パターン
初心者が陥りがちなパターンとして、次のようなものがあります。
1つ目は、「なんでも関数にすれば良い」と思って、意味のない分割をしてしまうことです。
たとえば以下のようなケースです。
function step1(price) { return price * 1.1; }
function step2(price) { return Math.floor(price); }
function calc(price) {
return step2(step1(price));
}
処理内容を見ないとstep1やstep2の意味が分からず、かえって読みづらくなっています。
「名前から意図が伝わらない抽象化」は、ない方がマシです。
2つ目は、逆に抽象化せずにコピペを繰り返すことです。
似たような処理が3回以上出てきたら、共通化(抽象化)を検討するサインだと考えてよいでしょう。
3つ目は、未来の要件を過度に想像して、使われない抽象化をたくさん作ってしまうことです。
「いつか役立つかも」と思って層を増やしすぎると、現在の要件に対しては過剰な複雑さになります。
良い抽象化を見つけるための観点
良い抽象化には、いくつかの共通した特徴があります。
コードを書くときに、次の観点をチェックしてみてください。
1つ目の観点は「名前を見ただけで役割が分かるか」です。
関数・クラス・モジュールの名前を見て「何をするものか」「どのレイヤの責務か」がイメージできるなら、その抽象化はかなり良い線をいっています。
2つ目の観点は「変更の影響範囲が限定されているか」です。
仕様変更があったときに、直すべき場所が明確で、小さな範囲に閉じているなら、その境界線(抽象化のライン)がうまく引けていると考えられます。
3つ目は「現実世界や業務の言葉と対応しているか」です。
ビジネスロジックなら、ドメインの言葉(注文、商品、在庫など)を適切にクラスや関数に落とし込めているかどうかが重要です。
現実の会話に出てこない謎の概念が増えていないかに注意しましょう。
抽象化レベルを上げすぎないためのチェックポイント
抽象化は強力ですが、やりすぎると「抽象化のための抽象化」になってしまうことがあります。
レベルを上げすぎていないかを確認するために、次のような点をチェックしてみてください。
- そのクラスや関数は、実際に複数箇所から使われているか
→ まったく再利用されていないなら、いったん統合してもよいかもしれません。 - 名前が「Manager」「Util」「Helper」など、意味があいまいになっていないか
→ 役割が曖昧な抽象化は、何でもかんでも押し込まれて肥大化しがちです。 - 読んだときに「これは具体的に何をしているのか?」と感じるレイヤが増えすぎていないか
→ レイヤが多すぎると、処理の流れを追うのが困難になります。

迷ったときは、いったん具体寄りにしておき、重複や複雑さが見えたところで抽象化する、というステップを踏むのがおすすめです。
抽象化力を鍛える練習方法
最後に、抽象化のセンスを磨くための具体的な練習方法をいくつか紹介します。
1つ目は「既存コードのリファクタリング」です。
自分や他人の書いた動いているコードを題材にして、次のような問いを投げかけてみてください。
- この処理は、どのようにグループ化できるか
- どの関数・クラスに責任を持たせると、変更に強くなりそうか
- 名前をどう変えれば意図がより明確に伝わるか
2つ目は「ライブラリ・フレームワークのAPI設計を観察する」ことです。
よく使うライブラリの関数名やクラス設計を「なぜこの名前なのか」「なぜこの粒度なのか」という視点で眺めてみると、抽象化のヒントがたくさん見つかります。
3つ目は「日常生活のタスクを抽象化してみる」ことです。
たとえば「朝の支度」をステップに分解し、さらに共通する処理をまとめる、などです。
- 具体: 目覚ましを止める、カーテンを開ける、顔を洗う、歯を磨く…
- 抽象: 起床処理、身支度処理 などにまとめる
こうした遊び感覚の練習でも、「どこで区切るか」「何を1つのまとまりとみなすか」を鍛えることができます。
まとめ
抽象化は、複雑な現実世界やプログラムの世界から、本質的な共通点を抜き出し、扱いやすい形に整理する思考ツールです。
日常生活でも無意識に行っていますが、プログラミングではそれを意識的・体系的に使うことで、次のようなメリットが得られます。
- コードの重複を減らし、読みやすく保守しやすくできる
- 条件分岐や依存関係を整理し、変更に強い設計にできる
- ライブラリやフレームワークの抽象化に乗ることで、ビジネスロジックに集中できる
一方で、抽象化しすぎると分かりづらくなったり、役に立たないレイヤが増えたりするため、「ちょうどよい抽象度」を探るバランス感覚も重要です。
そのためには、実際のコードを書き、リファクタリングし、既存の良い設計から学ぶというサイクルを繰り返すことが一番の近道です。
プログラミングの文法やフレームワークの使い方と同じか、それ以上に抽象化のセンスはエンジニアの生産性を左右するスキルです。
今日から「これはどう抽象化できるか」「どのレベルで考えるべきか」を意識しながらコードを書いてみてください。
少しずつ見える世界が変わり、コードの設計そのものを楽しめるようになっていきます。
