web-dev-qa-db-ja.com

dtoに継承があるのはアンチパターンですか?

データ転送オブジェクトまたはPOJOはfinalを意図したものですか、それとも拡張してそれらの階層を作成できますか?
そのような値クラスがfinalクラスとしてのみ適切に設計されているかどうかは不明ですが、DTOクラスの階層の良い例/設計は何ですか?

5
Jim

pdate:この質問はJavaとタグ付けされているため、コードをJavaで書き直しましたが、これはすべてのオブジェクト指向言語に適用されます。


継承の使用がコードの重複を減らす手段である場合、私はDTOが何からでも継承することに少し消極的です。継承は、子クラスと親の間のis-a関係を意味します。

例として監査フィールドを使用して、次のステートメントは正しいと思いますか?

PersonはBaseAuditFieldsです。

私がそれを大声で読んだとき、それは私にはちょっとおかしく聞こえます。 DTOの継承は問題になります。これは、各クラスが通常は具体的なアイデアまたはユースケースを表すクラス間に結合を導入するためです。共通の親クラスを共有するDTOは、クラス階層が同時に、同じ理由で進化することを意味します。これにより、新しいフィールドが適用されない可能性のあるクラスに波及する影響なしに、これらのクラスを変更することが難しくなります。

これは、他の会社やチームが所有するWebサービスなど、制御できないソースからのデータをシリアル化および逆シリアル化するDTOで頻繁に発生します。

そうは言っても、processingDTOを抽象化できるケースを見つけました。監査フィールドは良い例です。これらのケースでは、DTOが複数のインターフェースから継承できるという単純な事実から、インターフェースが具体的な親クラスよりもはるかに堅牢なソリューションであることがわかりました。それでも、インターフェイスをできる限り集中して、フィールドの小さなサブセットを取得または変更するための最小限のメソッドのみを定義します。

public interface Auditable {
    int getCreatorId();
    void setCreatorId(int creatorId);
    Integer getUpdaterId();
    void setUpdaterId(Integer updaterId);
}

public class Person implements Auditable {
    ...
}

ここで、is-aテストに戻ります。

人は監査可能です

ステートメントはより理にかなっています。インターフェイスを使用すると、DTOを処理するクラスでポリモーフィズムを利用しやすくなります。これで、ソース形式との間のシリアライゼーションとデシリアライゼーションに影響を与えることなく、DTOの処理に役立つと思われるインターフェースを自由に追加および削除できます。インターフェースが使用されているため、クラスを変更してデータの構造的な変更を反映し、DTOを処理するクラスにとってより適切なDTOにする追加のメソッドを使用できます。

監査フィールドに戻ると、1つのWebサービスがプロパティcreatorIdを呼び出し、別のサービスがcreateUserIdを呼び出しています。両方のDTOが同じインターフェースから継承する場合、両方の処理がより簡単になります。

営業スタッフのWebサービスがあるとします。偶然にも、XML応答のプロパティ名はAuditableインターフェースと一致しているため、このクラスでは何も追加する必要はありません。

public class SalesStaffWebServicePerson implements Auditable {
    private int creatorId;
    private Date createDate;
    private Integer updaterId;
    private Date updatedDate;

    public int getCreatorId() {
        return creatorId;
    }

    public void setCreatorId(int creatorId) {
        this.creatorId = creatorId;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Integer getUpdaterId() {
        return updaterId;
    }

    public void setUpdaterId(Integer updaterId) {
        this.updaterId = updaterId;
    }

    public Date getUpdatedDate() {
        return updatedDate;
    }

    public void setUpdatedDate(Date updatedDate) {
        this.updatedDate = updatedDate;
    }
}

顧客のWebサービスがJSON応答を返し、2つのフィールドの名前がAuditableインターフェースで定義されているものとは異なるため、互換性を持たせるために、このDTOにいくつかのゲッターとセッターを追加します。

public class CustomerWebServicePerson implements Auditable {
    private int createUserId;
    private Date createDate;
    private Integer updateUserId;
    private Date updatedDate;

    // Getters/setters mapped over from customer web service JSON response

    public int getCreateUserId() {
        return createUserId;
    }

    public void setCreateUserId(int createUserId) {
        this.createUserId = createUserId;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public Integer getUpdateUserId() {
        return updateUserId;
    }

    public void setUpdateUserId(Integer updateUserId) {
        this.updateUserId = updateUserId;
    }

    public Date getUpdatedDate() {
        return updatedDate;
    }

    public void setUpdatedDate(Date updatedDate) {
        this.updatedDate = updatedDate;
    }

    // Getters/setters to support Auditable interface

    public int getCreatorId() {
        return createUserId;
    }

    public void setCreatorId(int createUserId) {
        this.createUserId = createUserId;
    }

    public Integer getUpdaterId() {
        return updateUserId;
    }

    public void setUpdaterId(Integer updateUserId) {
        this.updateUserId = updateUserId;
    }
}

確かに、ゲッターとセッターが重複している可能性がありますが、舞台裏では同じフィールドを使用しているため、みんなが満足しています。

9
Greg Burghardt

DTOオブジェクトを扱うときは、逆シリアル化エンジンの制限を考慮する必要があります。基本オブジェクトがあることは理にかなっている場合がありますが、フープを飛び越えないと基本オブジェクトを利用できない場合があります。次の階層を例にとります。

public class Observation {
    public string SensorManufacturer { get; set; }
    public string SensorEquipmentId { get; set; }
    public float Reading { get; set; }
    public Location Position { get; set; }
}

public class BizObservation : Observation {
    public float BarSetting { get; set; }
}

この階層は、私がずっと前に契約のために書いた監視ソフトウェアに大まかに基づいていました。基本クラスには、観測精度、変化率、エラー頻度などを計算するために必要なすべてのものが含まれていました。存在する場合、計算の精度を向上させる追加情報を持つセンサーがいくつかあり、ソフトウェアは正しい観測オブジェクトの作成方法を知っていましたセンサーの製造元と機器IDに基づいています。

観測値を中央の場所に送信する必要があるため、観測値をシリアル化および逆シリアル化するために使用していたライブラリの機能に関する課題の1つがありました。

何がうまくいかないのですか?

  • すべてのライブラリがレコードをシリアル化できましたが、すべてが正しく逆シリアル化できるわけではありませんでした
  • 一部のデシリアライザーは、適切なクラスを選択するためにいくつかのカスタムヘルパーを必要としました

問題を克服することはできませんが、適切なサブクラスを選択するには、メッセージに情報を追加する必要がある可能性があります。多くのJSONライブラリーはそのままで問題なく機能しますが、ワイヤー形式がJSONでない場合は、距離が異なる場合があります。

ボトムライン

フラット階層は、最もスムーズで複雑でないDTOエクスペリエンスを提供します。

継承を追加すると複雑さが増しますが、シリアライゼーション/デシリアライゼーションライブラリによっては、その複雑さをそのまま使用できます。他のシステムでは、システムを確実に機能させるために、いくつかのフープをジャンプする必要があります。

3
Berin Loritsch

私は、監査フィールド(つまり、WhenAdded、WhenDeleted、IsActiveなど)を持つ基本のpojo/poco/dtoに対してケースが作られると思います。これらのフィールドがスキーマ/テーブル全体で共通である場合、問題ありません。また、使用しているORM(ある場合)と、派生したpojoの処理方法についても検討してください。

階層の例:

class BaseAuditFields
{
    Date LastUpdated;
    Date WhenCreated;
    String CreatedBy;
    Bool IsDeleted;
}

class Person extends BaseAuditFields
{
    String FirstName;
    String LastName;
}

class Pet extends BaseAuditFields
{
    String Species;
    String Name;
    String FavoriteToy;
}
2
Christopher