デザインパターンの必要性
コードの再利用性を高め、要件やシステムの変化に対応するためには、適切な設計が不可欠です。デザインパターンは、過去の経験から蓄積された最適な設計ソリューションを提供し、開発者がより効率的に高品質なソフトウェアを構築するための道筋を示します。
オブジェクト指向デザインパターン
オブジェクト指向プログラミングの三大メカニズムであるカプセル化、継承、ポリモーフィズムは、デザインパターンの基盤となります。コードの分離により、新機能の追加は新しいクラスを通じて実装され、既存のクラスへの変更や再コンパイルを最小限に抑えることができます。
例えば、基本的なウェブスクレイパークラスを考えてみましょう。このクラスには、リクエストの送信、データの抽出、ファイルへの書き込みという不変のプロセスがあります。これらのステップは、ProcessDataメソッドとして実装されます。一方、リクエストの送信先、データ抽出のルール、ファイルへの書き込み方法などは変更可能な部分です。
class WebScraperBase {
public:
bool ProcessData(const std::string& url) { // 不変のプロセス
auto extractedData = ExtractData(FetchContent(url));
SaveToFile(extractedData);
return true;
}
// 可変のメソッド
virtual std::string FetchContent(const std::string& url) {
std::string content;
std::cout << "WebScraperBase::FetchContent" << std::endl;
return content;
}
virtual std::vector<std::string> ExtractData(const std::string& content) {
std::vector<std::string> data;
std::cout << "WebScraperBase::ExtractData" << std::endl;
return data;
}
virtual bool SaveToFile(const std::vector<std::string>& data) {
std::cout << "WebScraperBase::SaveToFile" << std::endl;
return true;
}
};
WebScraperBaseクラスを変更せずに内部ロジックを変更するにはどうすればよいでしょうか?新しいクラスCustomWebScraperを作成し、WebScraperBaseを継承して可変のメソッドをオーバーライドします(これらのメソッドは仮想関数として定義されているため)。不変のメソッドであるProcessDataは、親クラスから直接継承できます。
class CustomWebScraper : public WebScraperBase {
public:
std::string FetchContent(const std::string& url) override {
std::string content;
std::cout << "CustomWebScraper::FetchContent" << std::endl;
return content;
}
std::vector<std::string> ExtractData(const std::string& content) override {
std::vector<std::string> data;
std::cout << "CustomWebScraper::ExtractData" << std::endl;
return data;
}
bool SaveToFile(const std::vector<std::string>& data) override {
std::cout << "CustomWebScraper::SaveToFile" << std::endl;
return true;
}
};
オブジェクト指向デザインパターンの分類
- 生成パターン(Creational Patterns):オブジェクトの生成プロセスを担当
- 構造パターン(Structural Patterns):クラスとオブジェクトの組み合わせを担当
- 振る舞いパターン(Behavioral Patterns):クラスとオブジェクト間の通信と責任の割り当てを担当
オブジェクト指向デザインの基本原則
単一責任の原則(Single Responsibility Principle, SRP)
前述のウェブスクレイパーの例では、三つの主要な機能(ネットワークアクセス、データ抽出、データ保存)があります。これらすべてを一つのクラスで実装することは、単一責任の原則に違反します。適切な設計では、各機能を担当する少なくとも三つのクラスが必要です。
開放閉鎖の原則(Open-Closed Principle, OCP)
拡張に対して開かれ、修正に対して閉じているべきです。機能を拡張する必要がある場合、既存のコードを修正するのではなく、新しいコードを追加するべきです。これにより、既存コードの修正時にエラーが発生するリスクを回避できます。重要な点は、既存のクラスを拡張するのではなく、新しいクラスを追加することです。これにより、コンパイル単位が相互に独立し、影響を最小限に抑えることができます。
リスコフの置換原則(Liskov Substitution Principle, LSP)
サブクラスは、プログラムの正確性に影響を与えることなく、基底クラスを置換できるべきです。例えば、Birdクラスにflyメソッドがある場合、飛べないダチョウクラスはこの原則を満たせません。このような場合は、Birdクラスを修正し、flyメソッドをmoveメソッドに置き換えるべきです。
依存性逆転の原則(Dependency Inversion Principle, DIP)
上位モジュールは下位モジュールに依存すべきではなく、両者は抽象に依存すべきです。抽象は詳細に依存すべきではなく、詳細は抽象に依存すべきです。
インターフェース分離の原則(Interface Segregation Principle, ISP)
これは依存性逆転の原則の発展形であり、大きなインターフェースをより小さく、より具体的なインターフェースに分割すべきです。例えば、ネットワークアクセスとデータ保存の両方の機能を持つスクレイパーを設計する場合、一つのクラスで両方のインターフェースを実装するのではなく、二つのクラスを設計してそれぞれのインターフェースを実装すべきです。これにより、拡張性が向上します。
合成/集約の再利用原則(Composition/Aggregation Reuse Principle, CARP)
「has-a」関係は「is-a」関係よりも優先されます。継承よりも、可能な限り合成(コンポジション)を使用すべきです。
デメテルの法則(Law of Demeter, LoD)
最小知識の原則とも呼ばれます。あるクラスが他のクラスについて知るべきことは最小限に抑えるべきです。これは、クラス間の結合度を低く保つことを意味します。コード実装においては、各クラスを適切にカプセル化し、不要な情報を公開しないようにします。例えば、正規表現を処理するクラスがそのデータをネットワーククラスに直接公開すべきではなく、必要なアクセスがある場合でもインターフェースを通じて行うべきです。