web-dev-qa-db-ja.com

「プレーンオールドデータ」クラスを使用する理由はありますか?

レガシーコードでは、データのラッパーにすぎないクラスがときどき見られます。何かのようなもの:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

OOについての私の理解は、クラスはデータの構造であり、そのデータを操作する方法であるということです。これはこの種類のオブジェクトを除外します。私にとっては、それらはstructsにすぎず、OOの目的を打ち負かすようなものです。コードのにおいかもしれませんが、それは必ずしも悪ではないと思います。

そのようなオブジェクトが必要になる場合がありますか?これが頻繁に使用される場合、デザインが疑わしいですか?

44
Michael K

間違いなく悪ではなく、コードは私の心のにおいがしません。データコンテナーは有効ですOO citizen。場合によっては、関連する情報を一緒にカプセル化したいことがあります。

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

より

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

クラスを使用すると、DoStuffWithBottleのすべての呼び出し元を変更することなく、ボトルに追加のパラメーターを追加することもできます。また、必要に応じて、Bottleをサブクラス化して、コードの可読性と構成をさらに向上させることができます。

たとえば、データベースクエリの結果として返されるプレーンデータオブジェクトもあります。その場合の用語は「データ転送オブジェクト」だと思います。

一部の言語では、他の考慮事項もあります。たとえば、C#では、構造体は値の型であり、クラスは参照型であるため、クラスと構造体の動作は異なります。

68
Adam Lear

データクラスは有効な場合があります。 DTOは、Anna Learが言及した1つの良い例です。ただし、一般的には、メソッドがまだ発芽していないクラスのシードと見なす必要があります。そして、古いコードでそれらの多くに遭遇している場合は、それらを強いコード臭として扱います。これらは、OOプログラミングに移行したことがなく、手続き型プログラミングのしるしである古いC/C++プログラマーによってよく使用されます。ゲッターとセッターに常に依存している(またはさらに悪いことに、非プライベートメンバーへの直接アクセス)は、気付かないうちに問題が発生する可能性があります。Bottleからの情報を必要とする外部メソッドの例を検討してください。

ここでBottleはデータクラスです):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

ここでは、Bottleにいくつかの動作を指定しました):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

最初の方法は、Tell-Don't-Askの原則に違反しており、Bottleのダムを維持することで、1つを脆弱にするもの(Cap)などのボトルに関する暗黙の知識を取り入れていますBottleクラス外のロジック。あなたが習慣的にゲッターに依存しているとき、あなたはこの種の「漏れ」を防ぐためにあなたのつま先にいる必要があります。

2番目の方法は、Bottleにその仕事に必要なものだけを要求し、Bottleを残して、壊れやすいか、または指定されたサイズより大きいかを判断します。その結果、メソッドとボトルの実装の間の結合がはるかに緩くなります。楽しい副作用は、メソッドがよりクリーンでより表現力豊かになることです。

オブジェクトの多くのフィールドを使用することはめったにありませんが、それらのフィールドを持つクラスに常駐する必要のあるロジックを記述する必要はありません。そのロジックが何であるかを理解し、それをそれが属する場所に移動します。

25
Mike E

これがあなたが必要とする種類のものであるならば、それは大丈夫ですが、ください、お願い、次のようにしてください

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

のようなものの代わりに

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

お願いします。

7
compman

@Annaが言ったように、間違いなく悪ではありません。確かに、クラスに操作(メソッド)を入れることができますが、これはwant toの場合のみです。 haveしないでください。

クラスに抽象化であるという考えとともに、操作をクラスに入れる必要があるという考えについて少し不満を感じてください。実際には、これはプログラマに

  1. 必要以上のクラスを作成します(冗長データ構造)。データ構造に必要最小限よりも多くのコンポーネントが含まれている場合、データ構造は正規化されていないため、矛盾した状態が含まれます。つまり、変更された場合、一貫性を保つために複数の場所で変更する必要があります。調整されたすべての変更を実行しないと、一貫性がなくなり、バグになります。

  2. notificationメソッドを入力して問題1を解決します。これにより、パーツAが変更された場合、必要な変更をパーツBとCに伝播しようとします。これが、get-andの使用が推奨される主な理由です-setアクセサメソッド。これは推奨される方法であるため、問題1の言い訳になり、問題1が多くなり、解決策2も多くなります。これにより、通知の実装が不完全なためにバグが発生するだけでなく、暴走通知のパフォーマンスが低下する問題が発生します。これらは無限の計算ではなく、非常に長い計算です。

これらの概念は、一般に、これらの問題に悩まされている100万行のモンスターアプリ内で作業する必要がなかった教師によって、良いこととして教えられています。

これが私がやろうとしていることです:

  1. データを可能な限り正規化し、データに変更が加えられたときに、可能な限り少ないコードポイントで行われるようにして、不整合な状態になる可能性を最小限に抑えます。

  2. データを非正規化する必要があり、冗長性が避けられない場合、一貫性を保つために通知を使用しないでください。むしろ、一時的な矛盾を許容します。それだけを行うプロセスによって、データを定期的にスイープすることで不整合を解決します。これにより、一貫性を維持する責任が一元化され、通知が発生しやすいパフォーマンスと正確性の問題が回避されます。その結果、コードははるかに小さく、エラーがなく、効率的です。

6
Mike Dunlavey

ゲームデザインでは、数千の関数呼び出しとイベントリスナーのオーバーヘッドにより、データのみを格納するクラスと、すべてのデータのみのクラスをループしてロジックを実行する他のクラスが必要になる場合があります。

3
AttackingHobo

アンナリアに同意する、

間違いなく悪ではなく、コードは私の心のにおいがしません。データコンテナは有効なOO市民です。関連情報を一緒にカプセル化したい場合があります。次のような方法がある方がはるかに良いです...

1999年のJavaコーディング規約を読むことを忘れることがあるので、この種のプログラミングが完全に問題ないことは明らかです。実際、それを回避すると、コードの臭いがします。 (ゲッター/セッターが多すぎる)

Javaからのコード規約1999:適切なパブリックインスタンス変数の1つの例は、クラスが本質的にデータ構造であり、動作がない場合です。つまり、クラスの代わりに構造体を使用した場合(Javaでサポートされている構造体の場合)、クラスのインスタンス変数をパブリックにすることが適切です- http://www.Oracle.com/technetwork/Java/javase/documentation/codeconventions-137265.html#177

POJOがEJBよりもよくあるように、正しく使用すると、POD(プレーンな古いデータ構造)はPOJOよりも優れています。
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

3
Richard Faber

この種のクラスは、中規模/大規模のアプリケーションを扱うときに、いくつかの理由で非常に役立ちます。

  • テストケースを簡単に作成して、データの一貫性を確保するのは非常に簡単です。
  • その情報に関わるあらゆる種類の動作を保持するため、データのバグ追跡時間が短縮されます
  • それらを使用すると、メソッドの引数を軽量に保つことができます。
  • ORMを使用する場合、このクラスは柔軟性と一貫性を提供します。クラスに既に存在する単純な情報に基づいて計算される複雑な属性を追加すると、1つの単純なメソッドの記述に役立ちます。これは、データベースをチェックし、すべてのデータベースに新しい変更が適用されていることを確認する必要があることよりも、はるかに俊敏で生産的です。

要約すると、私の経験では、それらは通常迷惑なよりも便利です。

3
guiman

構造体は、Javaであってもその場所があります。次の2つが当てはまる場合にのみ使用してください。

  • 動作のないデータを集計するだけです。パラメータとして渡す
  • 集計データがどんな種類の値を持つかは問題ではありません

この場合は、フィールドをパブリックにして、ゲッター/セッターをスキップする必要があります。とにかくゲッターとセッターは不格好であり、Javaは有用な言語のようなプロパティを持たないのはばかげています。構造体のようなオブジェクトはメソッドを持たないはずなので、パブリックフィールドが最も理にかなっています。

ただし、いずれかが当てはまらない場合は、実際のクラスを扱っています。つまり、すべてのフィールドはプライベートである必要があります。 (よりアクセスしやすいスコープのフィールドが絶対に必要な場合は、getter/setterを使用してください。)

想定した構造に動作があるかどうかを確認するには、フィールドがいつ使用されるかを確認します。 tell、do n't ask に違反しているように見える場合は、その動作をクラスに移動する必要があります。

一部のデータを変更してはならない場合は、それらすべてのフィールドをfinalにする必要があります。クラスを作成することを検討してください immutable 。データを検証する必要がある場合は、セッターとコンストラクターで検証を提供します。 (便利なトリックは、プライベートセッターを定義し、そのセッターのみを使用してクラス内のフィールドを変更することです。)

あなたのボトルの例はおそらく両方のテストに失敗するでしょう。次のようなコードを作成することができます。

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

代わりに

double volume = bottle.calculateVolumeAsCylinder();

高さと直径を変更した場合、それは同じボトルですか?おそらく違います。それらは最終的なものでなければなりません。直径は負の値でいいですか?ボトルは幅よりも高くなければなりませんか?キャップをnullにすることはできますか?番号?これをどのように検証していますか?クライアントが愚かであるか悪であると仮定します。 ( 違いを見分けるのは不可能です。 )これらの値を確認する必要があります。

新しいボトルクラスは次のようになります。

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
3
Eva

私見では、非常にオブジェクト指向のシステムでは、このような十分なクラスがないことがよくあります。私はそれを注意深く修飾する必要があります。

もちろん、データフィールドに広い範囲と可視性がある場合、コードベースにそのようなデータを改ざんする場所が数百または数千ある場合、それは非常に望ましくない可能性があります。それは不変条件を維持する問題と困難を求めています。しかし同時に、それはコードベース全体のすべての単一のクラスが情報隠蔽の恩恵を受けることを意味しません。

ただし、そのようなデータフィールドのスコープが非常に狭い場合が多くあります。非常に簡単な例は、データ構造のプライベートNodeクラスです。 Nodeが生データのみで構成されている場合、オブジェクトの相互作用の数を減らすことで、コードを大幅に簡略化できることがよくあります。代替バージョンでは、単にTree->Nodeとは対照的に、たとえばNode->TreeTree->Node Dataからの双方向結合が必要になる場合があるため、これは分離メカニズムとして機能します。

より複雑な例は、ゲームエンジンでよく使用されるエンティティコンポーネントシステムです。これらの場合、コンポーネントは、多くの場合、表示したもののような生データとクラスです。ただし、通常、その特定のタイプのコンポーネントにアクセスできるシステムは1つまたは2つしかないため、それらのスコープ/可視性は制限される傾向があります。その結果、これらのシステムの不変条件を維持するのは非常に簡単であることに気付く傾向があり、さらに、そのようなシステムはobject->object相互作用がほとんどないため、概観を簡単に把握できます。

そのような場合、相互作用が進む限り、このようなものになる可能性があります(この図は、結合ではなく相互作用を示しています。結合図には下の2番目の画像の抽象的なインターフェースが含まれている場合があるためです)。

enter image description here

...これとは対照的に:

enter image description here

...そして、前者のシステムは、依存関係が実際にデータに向かって流れているという事実にもかかわらず、維持するのがはるかに簡単であり、正確さの観点から推論する傾向があります。主に、オブジェクトが互いに相互作用して非常に複雑な相互作用のグラフを形成する代わりに、多くのものが生データに変換できるため、結合が大幅に少なくなります。

0
user204677