#10 オブジェクト指向プログラミング入門
まとめ——オブジェクト指向設計の全体像
シリーズのまとめ
ここまで9回にわたって、オブジェクト指向プログラミングの核となる概念を学んできました。
| 回 | テーマ | 要点 |
|---|---|---|
| 1 | クラスとオブジェクト | クラス=設計図、オブジェクト=実体 |
| 2 | カプセル化 | private でデータを隠し、不正な状態を防ぐ |
| 3 | 継承 | extends で共通コードを再利用・拡張する |
| 4 | ポリモーフィズム | 同じメソッド名で異なる動作、呼び出し側を単純に保つ |
| 5 | 抽象クラスとインターフェース | 設計の「約束」を強制する |
| 6 | 静的メソッドと定数 | インスタンスなしで使えるユーティリティ |
| 7 | 名前空間とオートローディング | 大きなプロジェクトでのクラス整理 |
| 8 | トレイト | 横断的な機能を継承なしで共有する |
| 9 | 依存性注入 | テストしやすく、差し替え可能な設計 |
SOLID原則——良い設計の指針
SOLID原則は、ロバート・C・マーティン(Uncle Bob)が提唱した5つのオブジェクト指向設計の指針です。これらを意識するとコードが保守しやすくなります。
S: 単一責任の原則(Single Responsibility Principle)
クラスは変更される理由を1つだけ持つべき
// NG: User クラスが複数の責任を持っている
class User
{
public function save(): void { /* DB保存 */ }
public function sendEmail(): void { /* メール送信 */ }
public function toHtml(): string { /* HTML出力 */ }
}
// OK: 責任を分割する
class User { /* データのみ */ }
class UserRepository { public function save(User $user): void {} }
class UserMailer { public function sendWelcome(User $user): void {} }
class UserPresenter { public function toHtml(User $user): string {} }
O: 開放・閉鎖の原則(Open/Closed Principle)
拡張に対して開き、修正に対して閉じている
// NG: 新しい支払い方法を追加するたびに修正が必要
function pay(string $type, int $amount): void {
if ($type === 'credit') { ... }
elseif ($type === 'bank') { ... }
// 追加するたびにここを変える
}
// OK: インターフェースで拡張できる設計(第4回で学んだポリモーフィズム)
interface Payable { public function pay(int $amount): string; }
// 新しい支払い方法は新クラスを追加するだけ。既存コードは変えない
L: リスコフ置換原則(Liskov Substitution Principle)
子クラスは親クラスと置き換えられなければならない
// NG: 正方形は長方形の一種だが、振る舞いが変わる
class Rectangle
{
public function setWidth(float $w): void { $this->width = $w; }
public function setHeight(float $h): void { $this->height = $h; }
public function area(): float { return $this->width * $this->height; }
}
class Square extends Rectangle
{
// 正方形は幅と高さが同じなのでセッターを変えてしまう
public function setWidth(float $w): void {
$this->width = $w;
$this->height = $w; // 高さも変わる! → 親の期待を裏切る
}
}
// OK: 継承ではなく別々のクラスにする、または Shape 抽象クラスで揃える
I: インターフェース分離の原則(Interface Segregation Principle)
使わないメソッドを持つインターフェースを強制してはならない
// NG: 巨大な1つのインターフェース
interface Animal {
public function walk(): void;
public function swim(): void; // 泳げない動物も実装が必要
public function fly(): void; // 飛べない動物も実装が必要
}
// OK: 小さなインターフェースに分割
interface Walkable { public function walk(): void; }
interface Swimmable { public function swim(): void; }
interface Flyable { public function fly(): void; }
class Duck implements Walkable, Swimmable, Flyable { ... }
class Dog implements Walkable, Swimmable { ... } // 飛べないので Flyable は不要
D: 依存性逆転の原則(Dependency Inversion Principle)
抽象(インターフェース)に依存し、具体(実装クラス)に依存しない(第9回で学んだDIの核心)
// NG: 具体クラスに依存
class OrderService
{
private SmtpMailer $mailer; // 具体クラスに依存
}
// OK: インターフェースに依存
class OrderService
{
private MailerInterface $mailer; // 抽象に依存
}
クラス設計のチェックリスト
クラスを作ったら以下を確認しましょう。
□ このクラスの責任は1つだけか?(SRP)
□ プロパティは適切に private/protected にしているか?(カプセル化)
□ 継承は「is-a」関係が成り立つか?
□ 外部に依存するものはインターフェース経由になっているか?(DIP)
□ テストしやすい構造になっているか(DIを使っているか)?
□ クラス名・メソッド名は意図を的確に表しているか?
□ 1メソッドの行数が長すぎないか(目安: 20行以内)?
□ コンストラクタ引数が多すぎないか(目安: 4個以下)?
よくある失敗パターンと改善例
パターン1: 神クラス(God Class)
// NG: 何でも知っている、何でもできるクラス
class AppManager
{
public function createUser() {}
public function deleteUser() {}
public function sendEmail() {}
public function generateReport() {}
public function processPayment() {}
public function calculateTax() {}
// ... 何百行も続く
}
// OK: 責任ごとにクラスを分割する
class UserService { ... }
class MailService { ... }
class ReportService { ... }
class PaymentService { ... }
class TaxCalculator { ... }
パターン2: アネミックドメインモデル
// NG: データしか持たないクラス(モデルに振る舞いがない)
class Order
{
public int $totalAmount;
public string $status;
// プロパティだけで、メソッドがない
}
// どこかのサービスクラスでロジックを全部書いている
class OrderService
{
public function calculateDiscount(Order $order): int { ... }
public function canCancel(Order $order): bool { ... }
public function complete(Order $order): void { ... }
}
// OK: ドメインロジックはモデル(クラス)が持つ
class Order
{
private int $totalAmount;
private string $status;
public function applyDiscount(int $percent): void
{
$this->totalAmount = (int)($this->totalAmount * (1 - $percent / 100));
}
public function canCancel(): bool
{
return $this->status === 'pending';
}
public function complete(): void
{
if ($this->status !== 'pending') {
throw new \RuntimeException('確定済みでない注文は完了できません。');
}
$this->status = 'completed';
}
}
パターン3: 深すぎる継承
// NG: 継承が深くなるとどこで何が定義されているかわからなくなる
class BaseModel extends BaseActiveRecord extends ActiveRecord extends Model { ... }
// OK: 継承は浅く保ち、機能の共有はトレイトやコンポジションで
class Post
{
use HasTimestamps, SoftDeletes;
// ...
}
PHP 8.x の便利な新機能まとめ
このシリーズで登場したPHP 8.xの機能を整理します。
// コンストラクタプロモーション(PHP 8.0)
class User {
public function __construct(
public readonly int $id,
public string $name,
) {}
}
// readonly プロパティ(PHP 8.1)
class Config {
public readonly string $appName;
}
// 名前付き引数(PHP 8.0)
$user = new User(id: 1, name: '田中太郎');
// ユニオン型(PHP 8.0)
function process(int|string $value): void {}
// nullsafe 演算子(PHP 8.0)
$city = $user?->getAddress()?->getCity();
// enum(PHP 8.1)
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
// インターセクション型(PHP 8.1)
function handle(Countable&Serializable $obj): void {}
// readonly クラス(PHP 8.2)
readonly class Point {
public function __construct(
public float $x,
public float $y,
) {}
}
次のステップ——デザインパターンへ
OOPの基礎を身につけたら、次はデザインパターンです。デザインパターンとは、繰り返し現れる設計上の問題に対する「名前のついた解決策」です。
Creational(生成)パターン
- Factory Method:オブジェクトの生成をサブクラスに委ねる
- Builder:複雑なオブジェクトを段階的に構築する
- Singleton:インスタンスを1つに限定する(第6回で紹介)
Structural(構造)パターン
- Decorator:オブジェクトに機能を動的に追加する
- Adapter:互換性のないインターフェースを繋ぐ
- Facade:複雑なサブシステムへのシンプルな窓口を作る
Behavioral(振る舞い)パターン
- Strategy:アルゴリズムを切り替え可能にする(ポリモーフィズムの応用)
- Observer:イベント駆動な通知の仕組み
- Command:処理をオブジェクトとして扱う
これらは「OOPの道具(継承・インターフェース・DI)を組み合わせてどう使うか」のレシピ集です。
最後に
10回のシリーズを通して学んできた内容は以上です。
OOPは「覚える」だけでは不十分で、実際にコードを書くことで初めて身につきます。このシリーズで学んだ概念を頭に入れながら、Laravelなどのフレームワークのソースコードを読んでみてください。クラス設計の実例が豊富に詰まっています。
良いコードは「動くコード」ではなく「変えやすいコード」です。その土台となるのがオブジェクト指向の考え方です。ぜひ実務や個人プロジェクトで実践してみてください。