単純なデータ構造
シンプルなデータ構造をシンプルにしてください! データが大量にあるだけの場合は、人為的な擬似カプセル化は必要ありません。
#include <iostream>
using namespace std;
class Unit {
public:
Unit(std::string name_, unsigned points_, int x_, int y_)
: name{
name_}, points{
points_}, x{
x_}, y{
y_} {
}
Unit(std::string name_) : name{
name_}, points{
0}, x{
0}, y{
0} {
}
Unit() : name{
""}, points{
0}, x{
0}, y{
0} {
}
void setName(std::string const& n) {
name = n; }
std::string const& getName() const {
return name; }
void setPoints(unsigned p) {
points = p; }
unsigned getPoints() const {
return points; }
void setX(int x_) {
x = x_; }
int getX() const {
return x; }
void setY(int y_) {
y = y_; }
int getY() const {
return x; }
private:
std::string name;
unsigned points;
int x;
int y;
};
int main(){
return 0;
}
getter
とを見るとsetter
、単なる定型文であることがわかります。オブジェクト指向プログラミングに関する書籍では、カプセル化について詳しく説明されていることがよくあります。これらは、データ メンバーごとにgetter
と を使用することを推奨しています setter
。
ただし、カプセル化は、一部のデータを自由なアクセスから保護する必要があることを意味します。通常、これは、いくつかのデータを結合するロジックがあることが原因です。この場合、アクセス関数がチェックを実行し、一部のデータは一緒にのみ変更される可能性があります。
しかし、C++
それは純粋なオブジェクト指向言語ではありません。場合によっては、構造は単なるデータのセットにすぎず、それ以上のものではありません。この事実を疑似クラスの背後に隠すのではなく、パブリック データ メンバーを含む構造体を使用して明確にすることをお勧めします。結果は同じです。誰もが何にでも無制限にアクセスできます。
このようなクラスは、ロジックが別の場所に隠された単なる単純なデータ コンテナであるように見えることがあります。ドメイン オブジェクトの場合、これは貧血ドメイン モデルと呼ばれ、一般にアンチパターンとみなされます。通常の解決策は、コードをリファクタリングし、データと共存するクラスにロジックを移動することです。
これを行うか、ロジックをデータから分離するかは、意識的に決定する必要があります。データをロジックから分離することにした場合は、おそらくその決定を書き留める必要があります。この場合、以前の結論に戻ります。クラスを使用する代わりに、共通データを持つ構造体を使用します。
ロジックをクラスに移動することにした場合でも、実際のカプセル化がクラスの外部で提供されることはまれです。例としては、pimpl
「イディオム」の詳細クラスが挙げられます。pimpl
これらのクラスには、それを含むクラスとそれ自体以外の誰もアクセスできないため、これらのgetter
合計をすべて加算することsetter
は意味がありません。
コンストラクターは、オブジェクトを一貫した状態で作成し、不変条件を確立するために必要になることがよくあります。通常のデータ構造の場合、維持できる不変条件や一貫性は存在しません。上記の例のコンストラクターは、デフォルトでオブジェクトを構築し、すぐにsetter
各メンバーを設定するために通過する必要はありません。よく見ると、単一引数のコンストラクターが明示的ではないため、:Any は暗黙的に変換できること
がわかるかもしれません。このようなことは、デバッグの楽しみと頭痛の種につながる可能性があります。最初から、クラス内初期化子の機能があります。この場合、コンストラクターの代わりに使用できます。上記のすべてのコンストラクターはこのメソッドにラップされています。このように、例の 53 行のコードは 6 行に要約されます。bug
std::string
Unit
C++11
struct Unit {
std::string name{
"" };
unsigned points{
0 };
int x{
0 };
int y{
0 };
};
均一な初期化を使用する場合、初期化は以前と同じようになります。
Unit a{
"Alice"};
Unit b{
"Bob", 43, 1, 2};
Unit c;
名前は空の文字列にしたり、特殊文字を含めたりすることはできません。それはすべて捨ててユニットを適切なクラスに作り直す必要があるという意味ですか?そうでないかもしれない。多くの場合、文字列などを検証してサニタイズするためのロジックが 1 か所にあります。プログラムまたはライブラリに入力されるデータはこのポイントを通過する必要があり、その後、データが有効であると見なされます。
これが貧血ドメイン モデルに近づきすぎても、すべてを Unit クラスに再度カプセル化する必要はありません。代わりに、ロジックを含むカスタム タイプを使用できますstd::string
。結局のところ、std::string
は任意の文字のセットです。何か違うものが必要な場合には便利かもしれませんstd::string
が、それは間違った選択です。カスタム型には適切なコンストラクターがある可能性があるため、デフォルトで空の文字列を構築することはできません。
クラスをもう一度見ると、x
と は y
ある種の座標であると推測できます。おそらくそれらは一緒に属しているので、この 2 つを結び付ける方法が必要ではないでしょうか。おそらくコンストラクターは両方を同時に設定することも、何も設定しないこともできるので意味があるのでしょうか?
いいえ、これは解決策ではありません。一部の症状は修正される可能性がありますが、「データ ブロック」コードの匂いがまだ残っています。これら 2 つの変数は一緒に属しているため、独自の構造体またはクラスを持つ必要があります。
struct Unit {
PlayerName name;
unsigned points{
0 };
Point location{
{
0,0} };
};
クラスに不変条件がある場合に使用しclass
、データ メンバーが独立して変化できる場合に使用しますstruct
。
例 1:
struct Pair {
// 成员可以独立变化
string name;
int volume;
};
例 2:
class Date {
public:
// 验证{yy, mm, dd}是有效的日期并初始化
Date(int yy, Month mm, char dd);
// ...
private:
int y;
Month m;
char d; // day
};
参考
[1]単純なデータ構造
[2] C++ コア ガイドライン C.2