C++で物理シミュレーションプログラムを書いています。私はOOPおよびC++の初心者です。
私のプログラムでは、入力ファイルのデータに基づいていくつかのオブジェクトを初期化する必要があります。
たとえば、架空の入力ファイル:
# Wind turbine input file:
number_of_blades = 2
hub_height = 120
# Airfoil data:
airfoil1 = { chord = 2, shape = naca0012}
airfoil2 = { chord = 3, shape = naca0016}
この例では、TurbineクラスとAirfoilクラスがあるとします。翼型オブジェクトは翼弦と形状を知る必要があり、タービンオブジェクトはブレードの高さと数を知る必要があります。
各オブジェクトが入力ファイルからそれ自体を構築できるように、これを行う必要がありますか?
例えば。:
class Turbine {
public:
Turbine(File input_file); // reads input file to get the number of blades
private:
int num_blades_;
double height_;
};
それとも無料の関数で行う必要があります:
Turbine create_turbine_from_file(File input_file)
{
Turbine t;
t.set_num_blades(input_file.parse_num_blades());
t.set_height(input_file.parse_height());
return t;
};
class Turbine {
public:
Turbine();
void set_height();
void set_num_blades();
private:
int num_blades_;
double height_;
};
各方法の長所と短所は何ですか?もっと良い方法はありますか?
まず第一に、プログラミングをさらに一歩進めて、それをより良くする方法について疑問に思ったこと(そして良い質問をしたこと)おめでとうございます。それは素晴らしい態度であり、あなたのプログラムをさらに一歩進めるために絶対に必要です。賞賛!
ここで扱っているのは、プログラムのアーキテクチャ(または、誰に尋ねるかによっては設計)に関連する問題です。 何をするかについてはそれほどではありませんが、どうやってそれをする(つまり、機能ではなくプログラムの構造)。これについて明確にすることは非常に重要です。totallyこれらのクラスにFile
オブジェクトを入力として受け取らせることができ、プログラムは引き続き機能します。 。さらに一歩進んですべての例外処理コードを追加し、ファイルとI/Oに関連するEdgeケースを処理した場合(する必要がありますどこかで実行されます)それらのクラス(...そこにはありません)で、I/Oとドメインロジック(ドメインロジックとは、解決しようとしている実際の問題に関連するロジックを意味します)の寄せ集めになり、プログラムは「機能」します。これを単純な1回限りのものにすることを計画している場合の目標は、それが機能すること適切にであることを意味します。うまくいけば、追加したい新機能やユースケースを見つけたときに、それほど難しくなく拡張してください。
OK、今、答え。最初:はい、Turbine
クラスのメソッドパラメータとしてファイルを使用すると、SRPに違反します。 Turbine
クラスとAirfoil
クラスは、ファイルについて何も知らないはずです。そして、はい、それを行うためのより良い方法があります。最初に行う1つの方法について説明し、その後、なぜそれが優れているのかについて詳しく説明します。これは単なる例であり(実際にはコンパイル可能なコードではなく、一種の擬似コード)、それを行うための1つの可能な方法であることを忘れないでください。
// TurbineData struct (to hold the data for turbines)
struct TurbineData
{
int number_of_blades;
double hub_height;
}
// TurbineRepository (abstract) class
class TurbineRepository
{
// Defines an interface for Turbine repositories, which return Vectors of TurbineData structures.
public:
virtual std::Vector<TurbineData> getAll();
}
// TurbineFileRepository class
class TurbineFileRepository: public TurbineRepository
{
// Implements the TurbineRepository "interface".
public:
TurbineRepository(File inFile);
std::Vector<TurbineData> getAll();
private:
File file;
}
TurbineFileRepository::TurbineFileRepository(File inFile)
{
// Process the File and handle everything you need to read from it
// At some point, do something like:
// file = inFile
}
std::Vector<TurbineData> TurbineFileRepository::getAll()
{
// Get the data from the file here and return it as a Vector
}
// TurbineFactory class
class TurbineFactory
{
public:
TurbineFactory(TurbineRepository *repo);
std::Vector<Turbine> createTurbines();
private:
TurbineRepository *repository;
}
TurbineFactory::TurbineFactory(TurbineRepository *repo)
{
// Create the factory here and eventually do something like:
// repository = repo;
}
TurbineFactory::createTurbines()
{
// Create a new Turbine for each of the structs yielded by the repository
// Do something like...
std::Vector<Turbine> results;
for (auto const &data : repo->getAll())
{
results.Push_back(Turbine(data.number_of_blades, data.hub_height));
}
return results;
}
// And finally, you would use it like:
int main()
{
TurbineFileRepository repo = TurbineFileRepository(/* your file here */);
TurbineFactory factory = TurbineFactory(&repo);
std::Vector<Turbines> my_turbines = factory.createTurbines();
// Do stuff with your newly created Turbines
}
さて、ここでの主なアイデアは、プログラムのさまざまな部分を互いに分離または非表示にすることです。特に、ドメインロジック(実際には問題をモデル化して解決するTurbine
クラス)があるプログラムのコア部分を、ストレージなどの他の詳細から分離したいと思います。最初に、外界からのTurbineData
sのデータを保持するTurbine
構造を定義します。次に、仮想メソッドを使用してTurbineRepository
抽象クラス(インスタンス化できないクラス、継承の親としてのみ使用されるクラス)を宣言します。これは、基本的に「TurbineData
構造を提供する」の動作を記述します。外の世界」。この抽象クラスは、インターフェース(動作の説明)とも呼ばれます。 TurbineFileRepository
クラスは、File
sに対してそのメソッドを実装します(したがって、その動作を提供します)。最後に、TurbineFactory
はTurbineRepository
を使用してそれらのTurbineData
構造を取得し、Turbine
sを作成します。
TurbineFactory -> TurbineRepo -> Turbine // with TurbineData as a means of passing data.
なぜ私はそれをこのようにしているのですか?プログラムの内部動作からファイルI/Oを分離する必要があるのはなぜですか?プログラムの設計またはアーキテクチャの2つの主な目標は、複雑さを軽減し、変更を分離することであるためです。複雑さを軽減するということは、個々の部分について適切かつ個別に推論できるように、物事を可能な限り単純にすることを意味します(ただし、単純ではありません)。Turbine
sについて考えるときは、次の形式について考える必要はありません。タービンデータを含むファイルが書き込まれるか、または読み取っているFile
がそこにあるかどうか。 Turbine
s、期間について考える必要があります。
変更の分離とは、変更がコード内の最小限の場所に影響を与えることを意味します。これにより、バグが発生する可能性(およびコードの変更後に発生する可能性のある領域)が最小限に抑えられます。また、頻繁に変更されるもの、または将来変更される可能性のあるものは、そうでないものとは別にする必要があります。たとえば、Turbine
データがファイルに格納されている形式が変更された場合、Turbine
クラスが変更される理由はなく、TurbineFileRepository
。 Turbine
を変更する必要がある唯一の理由は、より高度なモデリングを追加したか、基礎となる物理学が変更された(ファイル形式が変更される可能性よりもかなり低い)か、または類似したものである場合です。
データが保存される場所と方法の詳細は、TurbineFileRepository
などのクラスで個別に処理する必要があります。その結果、Turbine
sがどのように機能するかについてわからない 、またはなぜそれらが提供するデータが必要なのか。これらのクラスは、完全にすべきですI/O例外処理を実装し、プログラムが外の世界と対話するときに発生する、退屈で非常に重要なすべての種類の処理を実装します。しかし、それを超えるべきではありません。 TurbineRepository
の機能は、これらすべての詳細をTurbineFactory
から隠し、データのベクトルのみを提供することです。これはTurbineFileRepository
が実装するものでもあるため、TurbineData
構造を使用したい人に詳細を知らせる必要はありません。考えられる優れた機能変更として、タービンと翼のデータをMySQLデータベースに保存したいとします。それが機能するために必要なことは、TurbineDatabaseRepository
を実装してプラグインすることだけです。それ以上は必要ありません。かっこいい?
プログラミングで頑張ってください!
通常は無料の関数として実装する必要があります。その関数は通常operator>>
という名前で、2つの引数を取る必要があります:istream
とTurbine
への参照(そして渡されたistream
を返します) 。典型的な場合、それはクラスのfriend
になります。これは、(多くの場合)外部の世界が(直接)触れてはならない内部を直接操作できる必要があるためです。
class Turbine {
// ...
friend std::istream &operator>>(std::istream &is, Turbine &t) {
// Simplifying a bit here, but you get the idea.
return is >> t.num_blades_ >> t.height_;
}
};
これにより、SRPが満たされるだけでなく、クラスが他の標準ライブラリでも機能するようになります。たとえば、タービンの仕様(1つだけではない)でいっぱいのファイルを読みたい場合は、次のようにすることができます。
std::ifstream in("Turbines.txt");
std::vector<Turbine> turbines {
std::istream_iterator<Turbine>(in),
std::istream_iterator<Turbine>()
};