C++におけるクラスの基本概念
クラスとは何か:オブジェクト指向プログラミングの核心
クラスは、オブジェクト指向プログラミング(OOP)の中核となる概念で、データ(メンバ変数)と、そのデータを操作する関数(メンバ関数)をひとつの単位にまとめたものです。クラスを使用することで、以下のような利点が得られます:
- データとそれを操作する関数を論理的にグループ化できる
- カプセル化により、データの不正なアクセスを防ぐことができる
- コードの再利用性と保守性が向上する
基本的なクラスの定義例を見てみましょう:
class Person { private: // メンバ変数(データ) std::string name; int age; public: // コンストラクタ Person(const std::string& n, int a) : name(n), age(a) {} // メンバ関数(メソッド) void introduce() const { std::cout << "私の名前は" << name << "で、" << age << "歳です。" << std::endl; } // アクセサメソッド std::string getName() const { return name; } void setAge(int newAge) { age = newAge; } };
構造体とクラスの違い:アクセス制御とカプセル化
C++における構造体(struct)とクラス(class)の主な違いは、デフォルトのアクセス指定子にあります:
特徴 | struct | class |
---|---|---|
デフォルトのアクセス指定子 | public | private |
継承時のデフォルトアクセス指定子 | public | private |
メンバ変数とメンバ関数の定義 | 可能 | 可能 |
カプセル化の実現 | 可能 | 可能 |
実際の使用例で違いを確認してみましょう:
// structの例 struct Point { int x; // デフォルトでpublic int y; // デフォルトでpublic void move(int dx, int dy) { x += dx; y += dy; } }; // classの例 class Circle { double radius; // デフォルトでprivate Point center; // デフォルトでprivate public: Circle(double r, const Point& p) : radius(r), center(p) {} double getArea() const { return 3.14159 * radius * radius; } };
メンバ変数とメンバ関数の役割と使い方
メンバ変数とメンバ関数は、クラスの2つの主要な構成要素です。
メンバ変数(データメンバ)の特徴:
- オブジェクトの状態を表現する
- 適切なアクセス制御により保護される
- 生存期間はオブジェクトと同じ
class BankAccount { private: std::string accountNumber; // 口座番号 double balance; // 残高 std::string owner; // 口座所有者 public: // コンストラクタでメンバ変数を初期化 BankAccount(const std::string& num, const std::string& own) : accountNumber(num), balance(0.0), owner(own) {} // メンバ関数でメンバ変数を操作 void deposit(double amount) { if (amount > 0) { balance += amount; } } // constメンバ関数での参照 double getBalance() const { return balance; } };
メンバ関数(メソッド)の種類と用途:
- 通常のメンバ関数
void deposit(double amount); // オブジェクトの状態を変更可能
- const メンバ関数
double getBalance() const; // オブジェクトの状態を変更しない
- static メンバ関数
static double getExchangeRate(); // クラス全体に関連する操作
- 仮想メンバ関数
virtual void processTransaction(); // 継承時に上書き可能
メンバ関数の実装における重要なポイント:
- カプセル化された内部データの適切な操作
- 一貫性のある状態管理
- エラー処理とバリデーション
- const修飾子の適切な使用
クラスの設計では、以下の原則を考慮することが重要です:
- 単一責任の原則:クラスは1つの明確な責任のみを持つべき
- カプセル化の原則:内部データはできるだけprivateにする
- インターフェースの明確性:publicメンバ関数は使用方法が明確であるべき
- 状態の一貫性:メンバ関数は常にオブジェクトの有効な状態を維持するべき
これらの基本概念を理解し、適切に実装することで、保守性が高く、再利用可能なクラス設計が可能になります。
クラスの作成と初期化テクニック
コンストラクタの種類と便利
C++におけるコンストラクタには複数の種類があり、それぞれが異なる初期化シナリオに対応します。適切なコンストラクタの選択と実装は、クラスの使いやすさとパフォーマンスに大きく影響します。
1. デフォルトコンストラクタ
class Example { public: // 明示的なデフォルトコンストラクタ Example() : value(0) {} // または Example() = default; // コンパイラ生成のデフォルトコンストラクタを使用 private: int value; };
2. パラメータ付きコンストラクタ
class Rectangle { public: // パラメータ付きコンストラクタ Rectangle(double w, double h) : width(w), height(h) { validateDimensions(); } private: double width; double height; void validateDimensions() { if (width <= 0 || height <= 0) { throw std::invalid_argument("サイズは正の値である必要があります"); } } };
3. コピーコンストラクタ
class Resource { public: // コピーコンストラクタ Resource(const Resource& other) : data(new int(*other.data)) { std::cout << "コピーコンストラクタが呼ばれました" << std::endl; } private: std::unique_ptr<int> data; };
4. ムーブコンストラクタ
class Buffer { public: // ムーブコンストラクタ Buffer(Buffer&& other) noexcept : ptr(other.ptr), size(other.size) { other.ptr = nullptr; other.size = 0; } private: int* ptr; size_t size; };
初期化リストを使用した効率的なオブジェクト生成
初期化リストは、メンバ変数を効率的に初期化する方法を提供します。以下のような利点があります:
- パフォーマンスの向上(二重初期化の回避)
- const メンバ変数の初期化が可能
- 参照メンバの初期化が可能
class EffectiveInit { public: // 初期化リストを使用した効率的な初期化 EffectiveInit(std::string n, int v, const double& r) : name(std::move(n)) // ムーブセマンティクスの活用 , value(v) // 基本型の直接初期化 , reference(r) // 参照の初期化 , constValue(42) // const メンバの初期化 { // コンストラクタの本体は空でもOK } private: std::string name; int value; const double& reference; const int constValue; };
初期化リストのベストプラクティス:
- メンバ変数の宣言順序と初期化リストの順序を一致させる
- 可能な限り全てのメンバを初期化リストで初期化する
- 複雑な初期化ロジックはコンストラクタ本体に記述する
デストラクタの重要性とリソース管理
デストラクタは、オブジェクトが破棄される際のクリーンアップ処理を担当します。特に動的に確保したリソースの解放に重要です。
class ResourceManager { public: ResourceManager() : resource(new char[1024]) { std::cout << "リソースを確保しました" << std::endl; } // デストラクタ ~ResourceManager() { cleanup(); } // ムーブコンストラクタ ResourceManager(ResourceManager&& other) noexcept : resource(other.resource) { other.resource = nullptr; // 元のオブジェクトからリソースを移動 } // ムーブ代入演算子 ResourceManager& operator=(ResourceManager&& other) noexcept { if (this != &other) { cleanup(); // 既存のリソースを解放 resource = other.resource; other.resource = nullptr; } return *this; } // コピーを禁止 ResourceManager(const ResourceManager&) = delete; ResourceManager& operator=(const ResourceManager&) = delete; private: char* resource; void cleanup() { delete[] resource; resource = nullptr; } };
RAIIパターンの実践:
class FileHandler { public: FileHandler(const std::string& filename) : file(std::fopen(filename.c_str(), "r")) { if (!file) { throw std::runtime_error("ファイルを開けませんでした"); } } ~FileHandler() { if (file) { std::fclose(file); } } // ファイル操作メソッド bool readLine(std::string& line) { char buffer[256]; if (std::fgets(buffer, sizeof(buffer), file)) { line = buffer; return true; } return false; } private: std::FILE* file; };
初期化とリソース管理における重要なポイント:
- スマートポインタの活用
std::unique_ptr
を使用した排他的所有権の管理std::shared_ptr
を使用した共有リソースの管理
- 例外安全性の確保
- コンストラクタでの例外処理
- デストラクタは例外を投げないようにする
- ムーブセマンティクスの適切な実装
- リソースの所有権移転
- 不要なコピーの回避
- デストラクタでのクリーンアップ
- 確保したリソースの適切な解放
- nullptr チェックによる二重解放の防止
これらの技術を適切に組み合わせることで、メモリリークのない、効率的なリソース管理が実現できます。
クラスの継承と多態性の実践的な活用法
単一継承と複数継承の使いやすさ
C++における継承は、既存のクラスの機能を拡張または特殊化する強力な機能です。
単一継承の基本実装
// 基底クラス class Vehicle { protected: std::string brand; int year; public: Vehicle(const std::string& b, int y) : brand(b), year(y) {} virtual void startEngine() { std::cout << "エンジンを始動します" << std::endl; } // 純粋仮想関数 virtual double calculateFuelEfficiency() const = 0; virtual ~Vehicle() = default; }; // 派生クラス class Car : public Vehicle { private: int numberOfDoors; public: Car(const std::string& b, int y, int doors) : Vehicle(b, y), numberOfDoors(doors) {} void startEngine() override { Vehicle::startEngine(); // 基底クラスの処理を呼び出し std::cout << "車載システムを起動します" << std::endl; } double calculateFuelEfficiency() const override { // 車種特有の燃費計算ロジック return 15.5; // km/L } };
複数継承の実装と注意点
// インターフェース1 class ElectricPowered { public: virtual void chargeBattery() = 0; virtual int getBatteryLevel() const = 0; virtual ~ElectricPowered() = default; }; // インターフェース2 class PetrolPowered { public: virtual void refuelGas() = 0; virtual int getFuelLevel() const = 0; virtual ~PetrolPowered() = default; }; // ハイブリッド車クラス(複数継承) class HybridCar : public Vehicle, public ElectricPowered, public PetrolPowered { private: int batteryLevel; int fuelLevel; public: HybridCar(const std::string& b, int y) : Vehicle(b, y), batteryLevel(100), fuelLevel(100) {} // ElectricPoweredインターフェースの実装 void chargeBattery() override { batteryLevel = 100; std::cout << "バッテリーを充電しました" << std::endl; } int getBatteryLevel() const override { return batteryLevel; } // PetrolPoweredインターフェースの実装 void refuelGas() override { fuelLevel = 100; std::cout << "給油しました" << std::endl; } int getFuelLevel() const override { return fuelLevel; } double calculateFuelEfficiency() const override { // ハイブリッドシステムの燃費計算 return 30.0; // km/L } };
仮想関数とポリモーフィズムの実装テクニック
ポリモーフィズムを効果的に活用するためのテクニックを見ていきます。
1. 仮想関数テーブル(vtable)の理解
class Shape { public: virtual double getArea() const = 0; virtual double getPerimeter() const = 0; virtual void draw() const = 0; virtual ~Shape() = default; }; class Circle : public Shape { private: double radius; public: explicit Circle(double r) : radius(r) {} double getArea() const override { return 3.14159 * radius * radius; } double getPerimeter() const override { return 2 * 3.14159 * radius; } void draw() const override { std::cout << "円を描画: 半径 " << radius << std::endl; } }; // ポリモーフィックな処理 void processShape(const Shape& shape) { std::cout << "面積: " << shape.getArea() << std::endl; std::cout << "周囲長: " << shape.getPerimeter() << std::endl; shape.draw(); }
2. 仮想関数の実行時コストの最適化
class GameObject { public: // ホットパス上にある関数は非仮想に void update() { updatePosition(); // 非仮想関数 if (shouldUpdatePhysics()) { updatePhysics(); // 仮想関数 } } protected: // 頻繁に呼び出される処理は非仮想に void updatePosition() { x += velocityX; y += velocityY; } // 派生クラスごとに異なる処理は仮想関数に virtual void updatePhysics() = 0; virtual bool shouldUpdatePhysics() const = 0; private: double x, y; double velocityX, velocityY; };
抽象クラスとインターフェースの設計パターン
抽象クラスとインターフェースを使用した効果的な設計パターンを紹介します。
1. Strategy パターンの実装
// インターフェース class PaymentStrategy { public: virtual bool processPayment(double amount) = 0; virtual ~PaymentStrategy() = default; }; // 具体的な実装 class CreditCardPayment : public PaymentStrategy { public: bool processPayment(double amount) override { std::cout << "クレジットカードで " << amount << " 円を決済" << std::endl; return true; } }; class PayPalPayment : public PaymentStrategy { public: bool processPayment(double amount) override { std::cout << "PayPalで " << amount << " 円を決済" << std::endl; return true; } }; // コンテキストクラス class ShoppingCart { private: std::unique_ptr<PaymentStrategy> paymentStrategy; double total; public: void setPaymentStrategy(std::unique_ptr<PaymentStrategy> strategy) { paymentStrategy = std::move(strategy); } bool checkout() { if (paymentStrategy) { return paymentStrategy->processPayment(total); } return false; } };
2. Template Method パターンの実装
// 抽象基底クラス class DataProcessor { public: // テンプレートメソッド void processData() { loadData(); validateData(); transform(); save(); } protected: virtual void loadData() = 0; virtual void validateData() = 0; // フックメソッド(オプショナルな実装) virtual void transform() { // デフォルト実装 } virtual void save() = 0; }; // 具象クラス class CSVProcessor : public DataProcessor { protected: void loadData() override { std::cout << "CSVファイルを読み込み" << std::endl; } void validateData() override { std::cout << "CSV形式を検証" << std::endl; } void save() override { std::cout << "処理結果をCSVで保存" << std::endl; } };
継承と多態性を効果的に活用する際の重要なポイント:
- 継承の使用判断
- is-a関係が成り立つ場合のみ継承を使用
- 複数継承は慎重に検討
- 仮想関数の適切な使用
- パフォーマンスへの影響を考慮
- 純粋仮想関数と通常の仮想関数の使い分け
- インターフェース設計
- 単一責任の原則に従う
- 凝集度の高いインターフェースの作成
- 多態性の活用
- 型安全なポリモーフィズムの実装
- スマートポインタの使用
メモリ管理とパフォーマンス最適化
スマートポインタを活用したメモリリーク対策
スマートポインタは、モダンC++におけるメモリ管理の中核を担う機能です。適切に使用することで、メモリリークを防ぎながら安全なリソース管理が可能になります。
1. std::unique_ptrの活用
class ResourceManager { private: // 排他的所有権を持つリソース std::unique_ptr<Resource> resource; public: ResourceManager() : resource(std::make_unique<Resource>()) {} void processResource() { if (resource) { resource->process(); } } // リソースの移動 std::unique_ptr<Resource> transferOwnership() { return std::move(resource); } }; // ファクトリ関数での活用例 std::unique_ptr<Widget> createWidget(const std::string& type) { if (type == "basic") { return std::make_unique<BasicWidget>(); } else if (type == "advanced") { return std::make_unique<AdvancedWidget>(); } return nullptr; }
2. std::shared_ptrとweak_ptrの使用
class Observer { public: virtual void onUpdate() = 0; virtual ~Observer() = default; }; class Subject { private: // オブザーバーへの参照を保持 std::vector<std::weak_ptr<Observer>> observers; public: void addObserver(std::shared_ptr<Observer> observer) { observers.push_back(observer); } void notifyObservers() { // 期限切れの参照を自動的に削除 observers.erase( std::remove_if(observers.begin(), observers.end(), [](const std::weak_ptr<Observer>& obs) { return obs.expired(); }), observers.end() ); // 有効なオブザーバーに通知 for (const auto& weakObs : observers) { if (auto obs = weakObs.lock()) { obs->onUpdate(); } } } };
ムーブセマンティクスによる効率的なオブジェクト管理
ムーブセマンティクスを活用することで、不要なコピーを避け、パフォーマンスを向上させることができます。
class BigData { private: std::unique_ptr<std::vector<double>> data; size_t size; public: // コンストラクタ BigData(size_t n) : data(std::make_unique<std::vector<double>>(n)) , size(n) {} // ムーブコンストラクタ BigData(BigData&& other) noexcept : data(std::move(other.data)) , size(other.size) { other.size = 0; } // ムーブ代入演算子 BigData& operator=(BigData&& other) noexcept { if (this != &other) { data = std::move(other.data); size = other.size; other.size = 0; } return *this; } // 効率的なデータ転送 std::vector<double> extractData() { std::vector<double> result; if (data) { result = std::move(*data); size = 0; } return result; } };
コピーコンストラクタとムーブコンストラクタの最適化
オブジェクトのコピーとムーブ操作を最適化することで、アプリケーションのパフォーマンスを向上させることができます。
1. コピーの最適化
class OptimizedString { private: static const size_t SHORT_STRING_BUFFER = 16; union { char* longStr; char shortStr[SHORT_STRING_BUFFER]; }; size_t length; bool isLong; public: // 小さい文字列の場合はスタック上に保持 OptimizedString(const char* str) { length = strlen(str); if (length < SHORT_STRING_BUFFER) { isLong = false; strcpy(shortStr, str); } else { isLong = true; longStr = new char[length + 1]; strcpy(longStr, str); } } // コピーコンストラクタ OptimizedString(const OptimizedString& other) : length(other.length) , isLong(other.isLong) { if (isLong) { longStr = new char[length + 1]; strcpy(longStr, other.longStr); } else { memcpy(shortStr, other.shortStr, SHORT_STRING_BUFFER); } } // デストラクタ ~OptimizedString() { if (isLong) { delete[] longStr; } } };
2. パフォーマンス最適化テクニック
class DataProcessor { private: // メンバ変数のアライメント最適化 alignas(64) std::vector<double> data; std::vector<int> indices; public: // 事前予約によるメモリ再割り当ての防止 void prepare(size_t size) { data.reserve(size); indices.reserve(size); } // 効率的なデータ追加 void addData(double value, int index) { data.emplace_back(value); indices.emplace_back(index); } // 並列処理のための最適化 void processInParallel() { #pragma omp parallel for for (size_t i = 0; i < data.size(); ++i) { data[i] = std::pow(data[i], 2.0); } } };
最適化のためのベストプラクティス:
- メモリアロケーション最適化
- カスタムアロケータの使用
- メモリプールの実装
template<typename T> class PoolAllocator { private: std::vector<T*> freeList; static constexpr size_t POOL_SIZE = 1000; public: T* allocate() { if (freeList.empty()) { // プールに新しいブロックを追加 expandPool(); } T* ptr = freeList.back(); freeList.pop_back(); return ptr; } void deallocate(T* ptr) { freeList.push_back(ptr); } private: void expandPool() { for (size_t i = 0; i < POOL_SIZE; ++i) { freeList.push_back(new T()); } } };
- キャッシュ最適化
- データ構造のアライメント
- キャッシュフレンドリーなデータアクセス
class CacheOptimized { private: static constexpr size_t CACHE_LINE = 64; alignas(CACHE_LINE) std::array<double, 1024> data; public: void process() { // キャッシュラインを考慮したストライド for (size_t i = 0; i < data.size(); i += CACHE_LINE/sizeof(double)) { // データ処理 } } };
- リソース管理の最適化
- RAII原則の徹底
- スマートポインタの適切な使用
- メモリリークの防止
これらの最適化技術を適切に組み合わせることで、効率的で安全なメモリ管理が実現できます。
現場で使える高度なクラス設計テクニック
テンプレートを活用した汎用クラスの作成方法
テンプレートを使用することで、型に依存しない汎用的なクラスを設計できます。
1. 基本的なテンプレートクラス
template<typename T> class Container { private: std::vector<T> elements; public: void add(const T& element) { elements.push_back(element); } void add(T&& element) { elements.push_back(std::move(element)); } const T& get(size_t index) const { if (index >= elements.size()) { throw std::out_of_range("インデックスが範囲外です"); } return elements[index]; } // イテレータのサポート auto begin() { return elements.begin(); } auto end() { return elements.end(); } auto begin() const { return elements.begin(); } auto end() const { return elements.end(); } };
2. テンプレートの特殊化
// プライマリテンプレート template<typename T> class TypeHandler { public: static std::string getTypeName() { return "unknown"; } }; // int型の特殊化 template<> class TypeHandler<int> { public: static std::string getTypeName() { return "integer"; } }; // std::string型の特殊化 template<> class TypeHandler<std::string> { public: static std::string getTypeName() { return "string"; } };
3. 可変引数テンプレート
template<typename... Args> class EventDispatcher { private: std::function<void(Args...)> handler; public: void setHandler(const std::function<void(Args...)>& h) { handler = h; } void dispatch(Args... args) { if (handler) { handler(std::forward<Args>(args)...); } } }; // 使用例 EventDispatcher<int, std::string> dispatcher; dispatcher.setHandler([](int id, const std::string& message) { std::cout << "ID: " << id << ", Message: " << message << std::endl; }); dispatcher.dispatch(1, "Hello");
フレンド関数とフレンドクラスの適切な使用法
フレンド機能は、カプセル化を維持しながら、特定のクラスや関数にアクセス権を付与する方法を提供します。
class Matrix { private: std::vector<std::vector<double>> data; // フレンド関数の宣言 friend Matrix operator+(const Matrix& lhs, const Matrix& rhs); // フレンドクラスの宣言 friend class MatrixSerializer; public: Matrix(size_t rows, size_t cols) : data(rows, std::vector<double>(cols, 0.0)) {} double& at(size_t row, size_t col) { return data[row][col]; } const double& at(size_t row, size_t col) const { return data[row][col]; } }; // フレンド関数の実装 Matrix operator+(const Matrix& lhs, const Matrix& rhs) { // private メンバーに直接アクセス可能 Matrix result(lhs.data.size(), lhs.data[0].size()); for (size_t i = 0; i < lhs.data.size(); ++i) { for (size_t j = 0; j < lhs.data[0].size(); ++j) { result.data[i][j] = lhs.data[i][j] + rhs.data[i][j]; } } return result; } // フレンドクラスの実装 class MatrixSerializer { public: static void serialize(const Matrix& matrix, const std::string& filename) { // private メンバーに直接アクセス可能 std::ofstream file(filename); for (const auto& row : matrix.data) { for (double value : row) { file << value << " "; } file << "\n"; } } };
例外処理を考慮した安全なクラス設計
例外安全性を考慮したクラス設計は、信頼性の高いコードを作成する上で重要です。
1. 例外安全性の基本原則
class ResourceHolder { private: std::unique_ptr<Resource> resource; std::vector<Data> dataItems; public: // 強い例外保証を提供するメソッド void addItem(const Data& item) { // 一時オブジェクトを使用して例外安全性を確保 auto tempData = dataItems; tempData.push_back(item); // 例外が発生する可能性のある処理が成功した後で // 元のデータを更新 dataItems = std::move(tempData); } // 基本的な例外保証を提供するメソッド void processItems() { for (auto& item : dataItems) { try { item.process(); } catch (const std::exception& e) { // エラーログを記録 std::cerr << "処理エラー: " << e.what() << std::endl; // 部分的に処理を継続 continue; } } } };
2. RAII と例外処理の組み合わせ
class Transaction { private: Database& db; bool committed; public: explicit Transaction(Database& database) : db(database), committed(false) { db.beginTransaction(); } void commit() { db.commit(); committed = true; } ~Transaction() { if (!committed) { try { db.rollback(); } catch (...) { // デストラクタでは例外を抑制 std::cerr << "ロールバック中にエラーが発生しました" << std::endl; } } } }; // 使用例 void performDatabaseOperation(Database& db) { Transaction tx(db); // トランザクションを開始 try { // データベース操作 db.execute("INSERT INTO ..."); db.execute("UPDATE ..."); tx.commit(); // 成功時にコミット } catch (...) { // 例外発生時は自動的にロールバック throw; } }
3. 例外中立的なテンプレートクラス
template<typename T> class SafeContainer { private: std::vector<T> elements; mutable std::mutex mutex; public: // 例外中立的な追加操作 void add(const T& element) { std::lock_guard<std::mutex> lock(mutex); elements.push_back(element); // 例外が発生してもmutexは適切に解放される } // noexceptメソッド bool empty() const noexcept { std::lock_guard<std::mutex> lock(mutex); return elements.empty(); } // 条件付きnoexcept template<typename U = T> typename std::enable_if<std::is_nothrow_move_constructible<U>::value, U>::type removeFirst() noexcept { std::lock_guard<std::mutex> lock(mutex); if (elements.empty()) { return U{}; } U result = std::move(elements.front()); elements.erase(elements.begin()); return result; } };
高度なクラス設計における重要なポイント:
- テンプレートの活用
- 型の抽象化による再利用性の向上
- 特殊化による型固有の最適化
- SFINAE やコンセプトによる制約の実装
- フレンド機能の適切な使用
- カプセル化を破壊しない範囲での使用
- 必要最小限のアクセス権付与
- 明確な使用目的の定義
- 例外安全性の確保
- RAII パターンの活用
- 強い例外保証と基本的な例外保証の使い分け
- リソースの適切な管理
クラス設計のベストプラクティスとアンチパターン
SOLIDの原則に基づくクラス設計手法
SOLIDの各原則を実践的なコード例で解説します。
1. 単一責任の原則(Single Responsibility Principle)
// 悪い例 class UserManager { public: void createUser(const std::string& name) { /* ... */ } void saveToDatabase(const User& user) { /* ... */ } void sendEmail(const User& user) { /* ... */ } void generateReport() { /* ... */ } }; // 良い例 class UserManager { public: void createUser(const std::string& name) { /* ... */ } }; class UserRepository { public: void save(const User& user) { /* ... */ } }; class EmailService { public: void sendWelcomeEmail(const User& user) { /* ... */ } }; class ReportGenerator { public: void generateUserReport() { /* ... */ } };
2. オープン・クローズドの原則(Open-Closed Principle)
// インターフェース class Shape { public: virtual double calculateArea() const = 0; virtual ~Shape() = default; }; // 新しい図形を追加してもShapeクラスは変更不要 class Circle : public Shape { private: double radius; public: explicit Circle(double r) : radius(r) {} double calculateArea() const override { return 3.14159 * radius * radius; } }; class Rectangle : public Shape { private: double width; double height; public: Rectangle(double w, double h) : width(w), height(h) {} double calculateArea() const override { return width * height; } };
3. リスコフの置換原則(Liskov Substitution Principle)
class Bird { public: virtual void eat() = 0; virtual ~Bird() = default; }; class FlyingBird : public Bird { public: virtual void fly() = 0; }; class Sparrow : public FlyingBird { public: void eat() override { /* ... */ } void fly() override { /* ... */ } }; class Penguin : public Bird { // FlyingBirdは継承しない public: void eat() override { /* ... */ } }; // 正しい使用例 void feedBird(Bird& bird) { bird.eat(); // 全ての鳥で安全に動作 }
依存性注入とインターフェース分離の実践
依存性注入とインターフェース分離を実践的に適用する方法を示します。
1. 依存性注入の実装
// インターフェース class Logger { public: virtual void log(const std::string& message) = 0; virtual ~Logger() = default; }; class FileLogger : public Logger { public: void log(const std::string& message) override { // ファイルへのログ出力 } }; class ConsoleLogger : public Logger { public: void log(const std::string& message) override { std::cout << message << std::endl; } }; // 依存性注入を使用するクラス class OrderProcessor { private: std::shared_ptr<Logger> logger; public: // コンストラクタインジェクション explicit OrderProcessor(std::shared_ptr<Logger> log) : logger(std::move(log)) {} void processOrder(const Order& order) { // 注文処理 logger->log("注文処理完了: " + order.getId()); } };
2. インターフェース分離の実践
// 大きすぎるインターフェース(アンチパターン) class Worker { public: virtual void work() = 0; virtual void eat() = 0; virtual void sleep() = 0; virtual void calculateSalary() = 0; virtual void reportHours() = 0; virtual void takeVacation() = 0; }; // インターフェース分離の適用 class Workable { public: virtual void work() = 0; virtual ~Workable() = default; }; class Payable { public: virtual void calculateSalary() = 0; virtual void reportHours() = 0; virtual ~Payable() = default; }; class Employee : public Workable, public Payable { public: void work() override { /* ... */ } void calculateSalary() override { /* ... */ } void reportHours() override { /* ... */ } }; // 契約社員は給与計算が異なる class Contractor : public Workable { public: void work() override { /* ... */ } };
よくある設計ミスとその回避方法
一般的な設計ミスとその対策を解説します。
1. メンバ変数の過剰な公開
// アンチパターン class User { public: std::string name; // 直接アクセス可能 int age; // 直接アクセス可能 }; // 正しい実装 class User { private: std::string name; int age; public: // アクセサメソッドを提供 const std::string& getName() const { return name; } void setName(const std::string& n) { name = n; } int getAge() const { return age; } void setAge(int a) { if (a >= 0) { // 値の検証 age = a; } } };
2. 不適切なコピー/ムーブ意味論
// アンチパターン class ResourceManager { private: Resource* resource; public: ResourceManager() : resource(new Resource()) {} ~ResourceManager() { delete resource; } // コピー/ムーブ演算子が未定義 }; // 正しい実装 class ResourceManager { private: std::unique_ptr<Resource> resource; public: ResourceManager() : resource(std::make_unique<Resource>()) {} // コピーを禁止 ResourceManager(const ResourceManager&) = delete; ResourceManager& operator=(const ResourceManager&) = delete; // ムーブを許可 ResourceManager(ResourceManager&&) = default; ResourceManager& operator=(ResourceManager&&) = default; };
3. 不適切な継承関係
// アンチパターン class Array { public: virtual void add(int element) { /* ... */ } }; class Stack : public Array { // is-a関係が成立しない // Array の実装を再利用したいだけ }; // 正しい実装 class Stack { private: std::vector<int> elements; // コンポジションを使用 public: void push(int element) { elements.push_back(element); } };
クラス設計における重要なベストプラクティス:
- カプセル化の徹底
- プライベートメンバ変数の使用
- 適切なアクセサメソッドの提供
- 不変条件の保持
- インターフェースの設計
- 明確で使いやすいインターフェース
- 最小限の公開メンバ
- 一貫性のある命名規則
- リソース管理
- スマートポインタの活用
- RAII原則の遵守
- 適切なコピー/ムーブ意味論の実装
- エラー処理
- 例外安全性の確保
- 入力値の検証
- エラー状態の適切な伝播
- テスト容易性
- 依存性注入の活用
- モック可能なインターフェース
- 単一責任の原則の遵守