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


43

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

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

   getters/setters, maybe a constructor
}

オブジェクト指向についての私の理解は、クラスはデータの構造であり、そのデータを操作する方法だということです。これは、このタイプのオブジェクトを除外するようです。私にとって、それらはstructsオブジェクト指向の目的に勝るものではありません。コードの匂いかもしれませんが、必ずしも悪いとは思いません。

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


1
これは非常にあなたの質問に答えるが、それにもかかわらず、関連すると思われていません。stackoverflow.com/questions/36701/struct-like-objects-in-java
アダムリア

2
お探しの用語はPOD(Plain Old Data)です。
ガウラフ

9
これは、構造化プログラミングの典型的な例です。必ずしも悪いわけではなく、オブジェクト指向ではありません。
コンラッドルドルフ

1
これはスタックオーバーフローではありませんか?
ムアディブ

7
@ Muad'Dib:技術的に、プログラマーに関するものです。プレーンな古いデータ構造を使用する場合、コンパイラは気にしません。あなたのCPUはおそらくそれを楽しんでいます(「キャッシュから新鮮なデータの匂いが好き」など)。それはだにハングアップを取得「これは私の方法論は純度の低い作るのですか?」質問。
Shog9

回答:


67

間違いなく悪ではなく、コードの匂いも私の心にはありません。データコンテナーは有効なオブジェクト指向の市民です。関連情報を一緒にカプセル化したい場合があります。次のようなメソッドを持つ方がはるかに良いです

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#クラスでは、構造体は値型であり、クラスは参照型であるため、クラスと構造体の動作が異なります。


25
いや OOP のクラスのDoStuffWithメソッドある必要がありますBottle(また、おそらく不変であるべきです)。上記で書いたものは、オブジェクト指向言語での良いパターンではありません(レガシーAPIとのインターフェースがない限り)。それはある、しかし、非オブジェクト指向の環境で有効なデザイン。
コンラッドルドルフ

11
@Javier:それから、Javaも最良の答えではありません。Javaのオブジェクト指向への重点は圧倒的であり、基本的にこの言語は他に何も得意ではありません。
コンラッドルドルフ

11
@JohnL:もちろん単純化していた。しかし、一般的に、OOのオブジェクトはデータではなくstateをカプセル化します。これは素晴らしいことですが、重要な違いです。OOPのポイントは、多くのデータが存在しないことです。それはされたメッセージを送信する新しい状態を作成する状態の間で。メソッドレスオブジェクトとの間でメッセージを送信する方法がわかりません。(そして、これはOOPの元の定義なので、欠陥があるとは思わないでしょう。)
コンラッドルドルフ

13
@Konrad Rudolph:これが、メソッド内で明示的にコメントを作成した理由です。私は、Bottleのインスタンスに影響するメソッドをそのクラスに入れることに同意します。しかし、別のオブジェクトがBottleの情報に基づいて状態を変更する必要がある場合、デザインはかなり有効だと思います。
アダムリア

10
@ Konrad、doStuffWithBottleがボトルクラスに入るべきだとは思いません。なぜボトルはそれ自体で何かをする方法を知っている必要がありますか?doStuffWithBottleは、他の何かがボトルで何かをすることを示します。ボトルにそれが含まれている場合、それは密結合になります。ただし、BottleクラスにisF​​ull()メソッドがある場合、これはまったく適切です。
ネミ

25

データクラスは、場合によっては有効です。DTOは、Anna Learが言及した良い例です。ただし、一般的には、メソッドがまだ発芽していないクラスのシードと見なす必要があります。そして、古いコードでそれらの多くに出くわしている場合は、それらを強力なコードの匂いとして扱います。これらは、オブジェクト指向プログラミングに移行したことがなく、手続き型プログラミングの兆候である古い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脆弱であるか、指定されたサイズより大きいかを決定するために残します。その結果、メソッドとBottleの実装の間の結合がはるかに緩やかになります。心地よい副作用は、メソッドがよりクリーンで表現力が豊かになることです。

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


1
この答えに投票がないとは信じられません(まあ、あなたは今投票しています)。これは簡単な例かもしれませんが、オブジェクト指向が十分に悪用されると、クラスにカプセル化されているはずの多くの機能を含む悪夢に変わるサービスクラスを取得します。
アルプ

「OOに移行したことがない古いC / C ++プログラマによる」それはオブジェクト指向言語、Cの代わりにC ++を使用しての、すなわち全体のポイントだとC ++プログラマは、通常、かなりOOある
nappyfalcon

7

これがあなたが必要とするものである場合、それは問題ありませんが、どうぞ、お願いします、お願いします

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;
    }
}

お願いします。


14
これに関する唯一の問題は、検証がないことです。有効なボトルが何であるかについてのロジックは、Bottleクラスにある必要があります。ただし、提案された実装を使用すると、負の高さと直径を持つボトルを使用できます。オブジェクトを使用するたびに検証せずに、ビジネスルールをオブジェクトに適用する方法はありません。2番目の方法を使用することにより、Bottleオブジェクトがある場合、それは、契約に従って有効なBottleオブジェクトであり、常に有効になることを保証できます。
トーマスオーエンズ

6
これは、フィールドにアクセスする場合と同じ構文で検証ロジックを使用してプロパティアクセサーを追加できるため、.NETがプロパティよりもわずかに優れている1つの領域です。クラスがプロパティ値を取得できるが設定はできないプロパティを定義することもできます。
JohnL

3
@ user9521「悪い」値でコードが致命的なエラーを引き起こさないことが確実な場合は、メソッドに進みます。ただし、さらに検証、遅延読み込みを使用する機能、またはデータの読み取りまたは書き込み時に他のチェックが必要な場合は、明示的なゲッターとセッターを使用します。個人的には、変数を非公開にして、一貫性を保つためにゲッターとセッターを使用する傾向があります。このようにして、検証や他の「高度な」手法に関係なく、すべての変数が同じように扱われます。
ジョナサン

2
コンストラクターを使用する利点は、クラスを不変にすることがはるかに簡単になることです。これは、マルチスレッドコードを記述する場合に不可欠です。
フォーティランナー

7
可能な限りフィールドを最終的にします。私見私はデフォルトで最終的なフィールドを優先し、可変フィールドのキーワードを持っています。例var
ピーターローリー

6

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

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

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

  2. 通知メソッドを組み込むことで問題1を解決し、パートAが変更された場合、必要な変更をパートBおよびCに伝達しようとします。これが、アクセサーメソッドの取得および設定が推奨される主な理由です。これは推奨される方法であるため、問題1を弁解し、問題1と解決策2の多くを引き起こしているように見えます。これは、通知の実装が不完全なためにバグが発生するだけでなく、暴走した通知のパフォーマンスを低下させる問題にもなります。これらは無限計算ではなく、非常に長い計算です。

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

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

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

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


3

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

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

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


3

ゲームの設計では、1000の関数呼び出しのオーバーヘッド、およびイベントリスナーは、データのみを保存するクラスを持ち、すべてのデータのみのクラスをループしてロジックを実行する他のクラスを持つ価値がある場合があります。


3

アンナ・リアに同意し、

間違いなく悪ではなく、コードの匂いも私の心にはありません。データコンテナーは有効なオブジェクト指向の市民です。関連情報を一緒にカプセル化したい場合があります。次のようなメソッドを持つ方がはるかに良いです...

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

Java Code Conventions 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

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

  • 振る舞いのないデータを集約する必要があります。例えば、パラメータとして渡すためです。
  • 集約データがどのような値を持つかは少しも重要ではありません

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

ただし、これらのいずれかが当てはまらない場合は、実際のクラスを扱っています。つまり、すべてのフィールドはプライベートでなければなりません。(よりアクセスしやすい範囲のフィールドがどうしても必要な場合は、ゲッター/セッターを使用してください。)

想定される構造体に動作があるかどうかを確認するには、フィールドがいつ使用されるかを調べます。tellに違反しいると思われる場合は、質問しないで、その動作をクラスに移動する必要があります。

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

ボトルの例は、両方のテストに失敗する可能性が高くなります。次のような(工夫された)コードを使用できます。

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

代わりに

double volume = bottle.calculateVolumeAsCylinder();

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

これは、新しいBottleクラスの外観です。

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...

}

0

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

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

ただし、そのようなデータフィールドの範囲が非常に狭い場合が多くあります。非常に簡単な例はNode、データ構造のプライベートクラスです。多くの場合、単純Nodeに生データで構成できる場合は、実行されるオブジェクトの対話の数を減らすことで、コードを大幅に簡素化できます。すなわち、たとえば、双方向からの結合を必要とするかもしれない別のバージョンから減結合機構として、Tree->NodeおよびNode->TreeASは単に反対しますTree->Node Data

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

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

...そして、前者のタイプのシステムは、依存関係が実際にデータに向かって流れているという事実にもかかわらず、正確さの点で保守と推論がはるかに容易になる傾向があります。相互作用が非常に複雑なグラフを形成するオブジェクト同士が相互作用する代わりに、多くのものを生データに変換できるため、主にカップリングがはるかに少なくなります。


-1

OOPの世界には奇妙な生き物もたくさんいます。私はかつて抽象クラスを作成しましたが、これには何も含まれていません。他の2つのクラスの共通の親になるためだけです。それはPortクラスです。

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

したがって、SceneMemberは基本クラスであり、シーンに表示するために必要なすべてのジョブを実行します。追加、削除、IDの取得などです。メッセージはポート間の接続であり、独自の寿命があります。InputPortおよびOutputPortには、独自のI / O固有の機能が含まれています。

ポートが空です。これは、たとえばポートリストのためにInputPortとOutputPortをグループ化するために使用されます。SceneMemberにはメンバーとして機能するものがすべて含まれており、InputPortおよびOutputPortにはポートで指定されたタスクが含まれているため、空です。InputPortとOutputPortは非常に異なるため、共通の(SceneMemberを除く)関数はありません。したがって、ポートは空です。しかし、それは合法です。

たぶんそれはアンチパターンですが、私はそれが好きです。

(「妻」と「夫」の両方に使用される「mate」という言葉に似ています。具体的な人に「mate」という言葉を使用することはありません。彼/彼女が結婚しているかどうか、あなたは代わりに「誰か」を使用しますが、それはまだ存在し、まれな抽象的な状況、例えば法律のテキストで使用されます。


1
ポートがSceneMemberを拡張する必要があるのはなぜですか?SceneMembersで動作するポートクラスを作成してみませんか?
マイケルK

1
では、なぜ標準のマーカーインターフェイスパターンを使用しないのでしょうか?空の抽象基本クラスと基本的に同じですが、はるかに一般的なイディオムです。
TMN

1
@Michael:理論的な理由だけで、私は将来の使用のためにPortを予約しましたが、この未来はまだ到着しておらず、おそらく決して到着しません。名前がそうであるように、彼らには共通点がないとは気がつきませんでした。私は...任意の損失を被ったすべての人のための補償はその空のクラスによって発生した支払うことになる
ern0

1
@TMN:SceneMember(派生)タイプにはgetMemberType()メソッドがあり、SceneMemberオブジェクトのサブセットについてSceneがスキャンされる場合がいくつかあります。
ern0

4
これは質問に答えません。
エヴァ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.