抽象化とカプセル化の違いをTypeScriptの実例でわかりやすく示します。設計原則、リファクタリング手法、現場で使えるチェックリストを短く実践的にまとめました。
December 17, 2025 (8mo ago) — last updated July 15, 2026 (1mo ago)
TypeScriptで学ぶ抽象化とカプセル化
TypeScriptの実例で抽象化とカプセル化の違いを解説。リファクタリング手法、値オブジェクト、設計判断のチェックリスト付き。
← Back to blog
TypeScriptで学ぶ抽象化とカプセル化
決定版ガイド:抽象化(Abstraction)とカプセル化(Encapsulation)の違いを、実践的なTypeScript例と現実的なユースケースで解説します。クリーンコードの設計原則に沿ったリファクタリング手法とチェックリストを含みます。
はじめに
抽象化とカプセル化はオブジェクト指向設計の重要な概念ですが、目的は異なります。抽象化はコンポーネントが何をするかを明確に示し、複雑さを公開インターフェースの背後に隠します。カプセル化はオブジェクトの内部状態を保護し、状態変更を制御します。両方を適切に使うことで、予測可能でスケーラブルなシステムが作れます。
コアな違い:抽象化とカプセル化
- 抽象化:公開される契約(インターフェースや抽象クラス)で「何をするか」を定義する。
- カプセル化:データとそれを操作するメソッドをまとめ、外部が内部状態を直接変更するのを防ぐ。
抽象化は認知負荷を下げ、チームがモジュールを独立して開発できるようにします。一方、カプセル化は検証や不変条件を集中させ、データ整合性を守ります。TypeScriptの静的型とインターフェースは、この両方を強化するツールとして広く使われています1。
主要な持ち帰り:抽象化は“公開される顔”をつくり、カプセル化は“安全な内部”を守ります。
両者は補完関係にあります。堅牢なカプセル化があれば、破壊的変更を避けつつ抽象化を進化させられます。設計の比較は、例えばOOP vs Functional Programmingのガイドも参照してください。
抽象化が複雑さをどう簡潔にするか
大規模アプリケーションでは、適切な抽象化がないとコードが散らかります。インターフェースや抽象クラスは外部に公開する契約を定義し、消費側はその契約だけに依存できます。明確な抽象化レイヤーはコンポーネントの再利用性と保守性を高めると報告されています2。
決済ゲートウェイの契約を定義する(例)
複数の決済プロバイダ(例:Stripe、PayPal)を扱う場合、TypeScriptのインターフェースが便利です。
interface PaymentGateway {
processPayment(amount: number): Promise<{ success: boolean; transactionId: string }>
}
この契約はシステムが何を必要とするかを宣言し、各プロバイダはその契約を満たす実装を提供します。
具象クラスで内部をカプセル化する
プロバイダごとの詳細はクラス内に閉じ込めます。
class StripeGateway implements PaymentGateway {
async processPayment(amount: number): Promise<{ success: boolean; transactionId: string }> {
console.log(`Processing payment of $${amount} via Stripe...`)
const transactionId = `stripe_${Math.random().toString(36).substring(2)}`
return { success: true, transactionId }
}
}
class PayPalGateway implements PaymentGateway {
async processPayment(amount: number): Promise<{ success: boolean; transactionId: string }> {
console.log(`Processing payment of $${amount} via PayPal...`)
const transactionId = `paypal_${Math.random().toString(36).substring(2)}`
return { success: true, transactionId }
}
}
この構成では、システムの他の部分は具体的なプロバイダ実装に依存せず、新しいゲートウェイ追加も容易です。
カプセル化でデータ整合性を守る
カプセル化はプロパティとそれを操作するメソッドを結びつけ、外部が内部状態を破壊するのを防ぎます。検証や不変条件をメソッド内に集約することで、予測可能な振る舞いを維持できます。
UserProfileクラスの実践例
class UserProfile {
private _email: string
public readonly userId: string
constructor(userId: string, email: string) {
this.userId = userId
this.updateEmail(email)
}
public get email(): string {
return this._email
}
public updateEmail(newEmail: string): void {
if (!newEmail || !newEmail.includes('@')) {
throw new Error("Invalid email format provided.")
}
this._email = newEmail.toLowerCase()
console.log(`Email updated for user ${this.userId}`)
}
}
_emailがprivateであるため、すべての更新はupdateEmailを通して行われ、検証を保証できます。
管理されたアクセスの利点
- 保守性の向上:内部の検証ロジックを消費者に影響を与えず変更できる。
- 複雑さの削減:消費者は小さな公開サーフェスを使うだけで良い。
- セキュリティ強化:プライベート状態は機密データの誤使用を防ぐ。
両者の協調動作
抽象化は公開契約を定義し、カプセル化はその契約を満たす実装の詳細を隠します。例えば、Reactコンポーネントでデータ取得を行う場合、IApiServiceインターフェースを定義し、HTTPロジックをカプセル化するApiHandlerを実装すれば、コンポーネントは抽象化されたサービスに依存するだけになります。これによりテスト容易性と差し替え性が得られます。
export interface IApiService {
fetchData(endpoint: string): Promise<any>
}
export class ApiHandler implements IApiService {
private readonly baseUrl: string = 'https://api.example.com'
private readonly apiKey: string
constructor(apiKey: string) {
this.apiKey = apiKey
}
public async fetchData(endpoint: string): Promise<any> {
const response = await fetch(`${this.baseUrl}/${endpoint}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
}
コンポーネント側はIApiServiceに依存するだけなので、テスト用のモックや別実装へ簡単に差し替えられます。
よくあるコード臭の識別と修正
抽象化やカプセル化を誤ると、リーキーな抽象化、ゴッドオブジェクト、データクランプ、プリミティブ志向といったコード臭が発生します。これらはリファクタリングで改善できます。
リーキーな抽象化
消費者が実装の詳細を知る必要がある抽象化はリーキーです。高レベルのメソッドを追加し、インターフェースを強化して修正します。
ゴッドオブジェクト
単一責任原則に違反する大きなクラスは分割し、凝集した小さなクラスにすることで改善します。
リファクタリングチェックリスト
| Code Smell | Description | Refactoring Action |
|---|---|---|
| Leaky Abstraction | Abstraction exposes implementation details | Add higher-level methods and reinforce the interface |
| God Object | A class accumulates unrelated responsibilities | Decompose into smaller classes with single responsibilities |
| Data Clumps | Repeated groups of variables across code | Create a new class to encapsulate the group (e.g., DateRange) |
| Primitive Obsession | Using primitives for domain concepts | Create a value object (e.g., EmailAddress) |
プリミティブ志向の修正例
前:関数間で重複した検証ロジック。
function sendWelcomeEmail(email: string, content: string) {
if (!email.includes('@')) {
throw new Error('Invalid email format in sendWelcomeEmail!')
}
}
function updateUserProfile(userId: number, email: string) {
if (!email.includes('@')) {
throw new Error('Invalid email format in updateUserProfile!')
}
}
後:Emailを値オブジェクトにカプセル化。
class EmailAddress {
private readonly value: string
constructor(email: string) {
if (!email || !email.includes('@')) {
throw new Error('Invalid email format.')
}
this.value = email.toLowerCase()
}
public asString(): string {
return this.value
}
}
function sendWelcomeEmail(email: EmailAddress, content: string) {
// use email.asString()
}
function updateUserProfile(userId: number, email: EmailAddress) {
// use email.asString()
}
値オブジェクトにより検証が集中し、無効なデータがビジネスロジックへ到達するのを防げます。
AIペアプログラミングを強化するクリーンコード
明確な抽象化とカプセル化された実装は、AIコーディングアシスタントをより有用にします。AIは明確なインターフェースに基づいて意図を理解しやすくなり、カプセル化はAIがプライベートな状態を直接操作する提案を減らします3。
よくある疑問:抽象化とカプセル化
カプセル化は抽象化なしで成り立つか?
はい。クラスは状態を隠し、その対話メソッドを提供できます。ただし、公開インターフェースが乱雑だと効果的な抽象化とは言えません。
抽象化はインターフェースだけで実現するのか?
いいえ。適切に命名された関数、モジュール、サービスも抽象化を提供できます。重要なのは「使用者が何を見るべきか」を限定することです。
アクセス修飾子の役割は?
privateやpublicはカプセル化を実現するためのツールです。抽象化はどのメンバーを公開するかを設計する行為を指します。
短いQ&A(要点まとめ)
Q1: 抽象化とカプセル化の見分け方は?
A1: 抽象化は「これは何をするか?」、カプセル化は「内部状態はどう守られているか?」に答えます。
Q2: TypeScriptではいつインターフェースを使い、いつクラスを使う?
A2: インターフェースは契約定義、クラスは振る舞い実装と状態のカプセル化に使います。疎結合でテストしやすくしたい箇所はインターフェース優先です。
Q3: リーキーな抽象化やゴッドオブジェクトをどう見つける?
A3: 消費側が実装の詳細に依存している箇所、長いメソッド一覧、無関係な責務を持つクラスは要リファクタリングのサインです。
実践Q&A(導入・運用の悩みに対する簡潔回答)
Q: 抽象化を増やしすぎて複雑になったらどうする?
A: 抽象化の目的を確認し、実際の使用シナリオに基づいて不要なレイヤーを削除または統合します。
Q: カプセル化でテストしにくくなるときの対処は?
A: 依存性注入やインターフェースを使って内部実装をモック可能にします。
Q: どのレベルで値オブジェクトを導入するべき?
A: 同じ検証ロジックが複数箇所に現れる、またはドメイン概念が明確な場合に導入します。
設計判断Q&A(設計レビューで使える短答)
Q: 公開するメソッドが増えすぎている場合は?
A: 責務を分割し、内部APIはprivateにして公開サーフェスを最小化します。
Q: 新しい実装を安全に追加したいときはどうする?
A: 既存のインターフェースを保ちながら具象クラスを追加し、カプセル化されたテストで差し替え検証を行います。
Q: プリミティブ志向が疑われるときの最速対処は?
A: 値オブジェクトを導入して検証ロジックを集中させます。
導入とテストQ&A(移行・導入時のポイント)
Q: 既存コードで抽象化を追加する際の優先順位は?
A: 変更頻度が高くテストが必要な箇所から。まずは外部依存の抽象化を作ります。
Q: モジュールを小さく保つ基準は?
A: 単一責任原則、明確なテスト単位、そして再利用可能性です。
Q: AIツールと併用するときの注意点は?
A: インターフェース設計を明確にし、機密情報をプライベートに保つことで、AIの提案が安全で有用になります3。
関連記事
AIがコードを書きます。あなたがそれを長持ちさせます。
AI加速の時代において、クリーンコードは単なる良い実践ではありません—スケールするシステムと自らの重みで崩壊するコードベースの違いです。