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 以降のパターン設計の土台になる