OOP原則

クラス設計の軸になる SOLID と依存の切り離しを押さえる。

SOLID原則

  • Single Responsibility: 1クラス1責任
  • Open/Closed: 拡張に開放、修正に閉鎖
  • Liskov Substitution: 派生クラスは基底クラスの代わりに使える
  • Interface Segregation: 必要な機能だけを分けて公開
  • Dependency Inversion: 具体ではなく抽象に依存

Single Responsibility

1つのクラスに複数の役割を持たせると、修正の影響が広がります。 役割を分けると、変更理由が1つになります。

#include <iostream>
#include <string>

// 悪い例: 1つのクラスに責務が多すぎる
class User {
private:
    std::string email;

public:
    void validateEmail() {
        std::cout << "メール検証" << std::endl;
    }

    void saveToDatabase() {
        std::cout << "DB保存" << std::endl;
    }

    void sendEmail() {
        std::cout << "メール送信" << std::endl;
    }
};

// 良い例: 役割を分離
class UserProfile {
private:
    std::string email;

public:
    explicit UserProfile(const std::string& e) : email(e) {}
    std::string getEmail() const { return email; }
};

class UserRepository {
public:
    void save(const UserProfile& user) {
        std::cout << "DB保存: " << user.getEmail() << std::endl;
    }
};

class EmailService {
public:
    void send(const UserProfile& user) {
        std::cout << "メール送信: " << user.getEmail() << std::endl;
    }
};

int main() {
    UserProfile user("taro@example.com");
    UserRepository repository;
    EmailService emailService;

    repository.save(user);
    emailService.send(user);
    return 0;
}

DB保存: taro@example.com
メール送信: taro@example.com

Open/Closed原則

既存コードをなるべく変更せず、拡張だけで振る舞いを増やすのが理想です。

#include <iostream>
#include <memory>
#include <vector>

class Report {
public:
    virtual ~Report() = default;
    virtual void generate() = 0;
};

class PDFReport : public Report {
public:
    void generate() override {
        std::cout << "PDF生成" << std::endl;
    }
};

class ExcelReport : public Report {
public:
    void generate() override {
        std::cout << "Excel生成" << std::endl;
    }
};

class HTMLReport : public Report {
public:
    void generate() override {
        std::cout << "HTML生成" << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Report>> reports;
    reports.push_back(std::make_unique<PDFReport>());
    reports.push_back(std::make_unique<ExcelReport>());
    reports.push_back(std::make_unique<HTMLReport>());

    for (const auto& report : reports) {
        report->generate();
    }
    return 0;
}

PDF生成
Excel生成
HTML生成

Dependency Inversion

具体実装に直接依存すると、差し替えやテストが難しくなります。 抽象インターフェース経由にすると柔らかい設計になります。

#include <iostream>
#include <memory>
#include <string>

class Database {
public:
    virtual ~Database() = default;
    virtual void query(const std::string& sql) = 0;
};

class MySQLDatabase : public Database {
public:
    void query(const std::string& sql) override {
        std::cout << "MySQL: " << sql << std::endl;
    }
};

class MockDatabase : public Database {
public:
    void query(const std::string& sql) override {
        std::cout << "MockDB: " << sql << std::endl;
    }
};

class UserService {
private:
    std::unique_ptr<Database> db;

public:
    explicit UserService(std::unique_ptr<Database> database)
        : db(std::move(database)) {}

    void loadUser(int id) {
        db->query("SELECT * FROM users WHERE id = " + std::to_string(id));
    }
};

int main() {
    UserService production(std::make_unique<MySQLDatabase>());
    production.loadUser(42);

    UserService test(std::make_unique<MockDatabase>());
    test.loadUser(99);
    return 0;
}

MySQL: SELECT * FROM users WHERE id = 42
MockDB: SELECT * FROM users WHERE id = 99

Interface Segregation

大きすぎるインターフェースは使う側を困らせます。用途ごとに分けると扱いやすくなります。

class Printable {
public:
    virtual ~Printable() = default;
    virtual void print() const = 0;
};

class Savable {
public:
    virtual ~Savable() = default;
    virtual void save() const = 0;
};

class Document : public Printable, public Savable {
public:
    void print() const override {
        std::cout << "印刷" << std::endl;
    }

    void save() const override {
        std::cout << "保存" << std::endl;
    }
};

よくある誤り

誤り1: 1つのクラスに保存・通知・計算を全部入れる

誤り2: 具体クラスを new して直接差し込む

誤り3: 巨大なインターフェースをそのまま使わせる

ポイント

  • SOLID は設計のチェックリストとして使える
  • 責務分離は保守とテストを楽にする
  • 抽象に依存すると差し替えやすくなる
  • インターフェースは小さく分ける方が扱いやすい

やってみよう

練習1: 商品クラスと在庫管理クラスを分離する

練習2: PDF/CSV/HTML の出力を Report インターフェースで統一する

練習3: Database をモックに差し替えてテスト用に使う

まとめ

  • OOP原則は設計の共通言語になる
  • 責務・拡張・依存の3点を意識すると崩れにくい
  • Phase 8 以降のパターン設計の土台になる