web-dev-qa-db-ja.com

スケーラビリティとテスト容易性を考慮しながら、ドメインエンティティをDTOに適切に変換する方法

ドメインオブジェクトをDTOに変換するためのいくつかの記事とStackoverflowの投稿を読んで、コードで試しました。テストとスケーラビリティに関しては、常にいくつかの問題に直面しています。ドメインオブジェクトをDTOに変換するための次の3つの可能なソリューションを知っています。ほとんどの場合、私はSpringを使用しています。

解決策1:変換のためのサービス層のプライベートメソッド

最初の可能な解決策は、取得したデータベースオブジェクトをDTOオブジェクトに変換するサービスレイヤーコードに小さな「ヘルパー」メソッドを作成することです。

_@Service
public MyEntityService {

  public SomeDto getEntityById(Long id){
    SomeEntity dbResult = someDao.findById(id);
    SomeDto dtoResult = convert(dbResult);
    // ... more logic happens
    return dtoResult;
  }

  public SomeDto convert(SomeEntity entity){
   //... Object creation and using getter/setter for converting
  }
}
_

長所:

  • 実装が簡単
  • 変換のための追加のクラスは必要ありません->プロジェクトはエンティティで爆発しません

短所:

  • new SomeEntity()がprivatedメソッドで使用され、オブジェクトが深くネストされている場合、変換も解消する場合はNullPointersを回避するためにwhen(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject)の適切な結果を提供する必要があるため、テスト時の問題入れ子構造

解決策2:ドメインエンティティをDTOに変換するためのDTOの追加コンストラクター

2番目の解決策は、DTOエンティティにコンストラクタを追加して、コンストラクタ内のオブジェクトを変換することです。

_public class SomeDto {

 // ... some attributes

 public SomeDto(SomeEntity entity) {
  this.attribute = entity.getAttribute();
  // ... nesting convertion & convertion of lists and arrays
 }

}
_

長所:

  • 変換のための追加のクラスは必要ありません
  • dTOエンティティに隠された変換->サービスコードが小さい

短所:

  • サービスコードでnew SomeDto()を使用するため、someDaoモックの結果として正しいネストされたオブジェクト構造を提供する必要があります。

解決策3:この変換にSpringのコンバーターまたはその他の外部化Beanを使用する

Springが変換の理由でクラスを提供していることが最近見られた場合:_Converter<S, T>_が、このソリューションは変換を行うすべての外部化されたクラスを表しています。このソリューションでは、コンバーターをサービスコードに挿入し、ドメインエンティティをDTOに変換するときに呼び出します。

長所:

  • テストケース中に結果をモックできるのでテストが簡単
  • タスクの分離->専用のクラスが仕事をしています

短所:

  • 私のドメインモデルが大きくなるほど「スケーリング」しません。多くのエンティティでは、新しいエンティティごとに2つのコンバータを作成する必要があります(-> DTOエンティティとエンティティをDTOに変換)

私の問題に対するより多くの解決策があり、どのようにそれを処理しますか?新しいドメインオブジェクトごとに新しいコンバーターを作成し、プロジェクト内のクラスの量に「対応」できますか?

前もって感謝します!

19
rieckpil

解決策1:変換のためのサービス層のプライベートメソッド

DTOはドメイン指向であり、サービス指向ではないため、ソリューション1はうまく機能しないと思います。したがって、異なるサービスで使用される可能性があります。そのため、マッピングメソッドは1つのサービスに属さないため、1つのサービスに実装しないでください。別のサービスでマッピング方法をどのように再利用しますか?

サービスごとに専用のDTOを使用する場合、ソリューションはうまく機能します。しかし、これについては最後に詳しく説明します。

解決策2:ドメインエンティティをDTOに変換するためのDTOの追加コンストラクター

DTOはエンティティへのアダプタとして表示できるため、一般的には適切なオプションです。つまり、DTOはエンティティの別の表現です。このような設計では、ソースオブジェクトをラップし、ラップされたオブジェクトの別のビューを提供するメソッドを提供することがよくあります。

ただし、DTOはデータtransferオブジェクトであるため、遅かれ早かれシリアル化され、ネットワーク経由で送信される場合があります。 springのリモート機能 を使用します。この場合、このDTOを受け取るクライアントは、DTOのシリアル化を解除する必要があるため、DTOのインターフェイスのみを使用する場合でも、そのクラスパスにエンティティクラスが必要です。

解決策3:この変換にSpringのコンバーターまたはその他の外部化Beanを使用する

ソリューション3は、私も好むソリューションです。しかし、私はMapper<S,T>ソースからターゲットへ、またはその逆へのマッピングを担当するインターフェイス。例えば。

public interface Mapper<S,T> {
     public T map(S source);
     public S map(T target);
}

modelmapper のようなマッピングフレームワークを使用して実装を行うことができます。


また、あなたは、各エンティティのコンバーター

私のドメインモデルが大きくなるほど「スケーリング」しません。多くのエンティティでは、新しいエンティティごとに2つのコンバータを作成する必要があります(-> DTOエンティティとエンティティをDTOに変換)

DTOはドメイン指向であるため、1つのDTOに対して2つのコンバーターまたは1つのマッパーを作成するだけで済みます。

別のサービスで使用し始めるとすぐに、他のサービスは通常、最初のサービスが行うすべての値を返す必要がある、または返せないことを認識します。他のサービスごとに別のマッパーまたはコンバーターの実装を開始します。

専用または共有DTOの長所と短所から始めると、この答えは長くなるので、ブログを読むように頼むことができます サービス層設計の長所と短所

14
René Link

私は受け入れられた答えからの3番目の解決策が好きです。

解決策3:この変換にSpringのコンバーターまたはその他の外部化Beanを使用する

そして、この方法でDtoConverterを作成します。

BaseEntityクラスマーカー:

public abstract class BaseEntity implements Serializable {
}

AbstractDtoクラスマーカー:

public class AbstractDto {
}

GenericConverterインターフェイス:

public interface GenericConverter<D extends AbstractDto, E extends BaseEntity> {

    E createFrom(D dto);

    D createFrom(E entity);

    E updateEntity(E entity, D dto);

    default List<D> createFromEntities(final Collection<E> entities) {
        return entities.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

    default List<E> createFromDtos(final Collection<D> dtos) {
        return dtos.stream()
                .map(this::createFrom)
                .collect(Collectors.toList());
    }

}

CommentConverterインターフェイス:

public interface CommentConverter extends GenericConverter<CommentDto, CommentEntity> {
}

CommentConveterクラスの実装:

@Component
public class CommentConverterImpl implements CommentConverter {

    @Override
    public CommentEntity createFrom(CommentDto dto) {
        CommentEntity entity = new CommentEntity();
        updateEntity(entity, dto);
        return entity;
    }

    @Override
    public CommentDto createFrom(CommentEntity entity) {
        CommentDto dto = new CommentDto();
        if (entity != null) {
            dto.setAuthor(entity.getAuthor());
            dto.setCommentId(entity.getCommentId());
            dto.setCommentData(entity.getCommentData());
            dto.setCommentDate(entity.getCommentDate());
            dto.setNew(entity.getNew());
        }
        return dto;
    }

    @Override
    public CommentEntity updateEntity(CommentEntity entity, CommentDto dto) {
        if (entity != null && dto != null) {
            entity.setCommentData(dto.getCommentData());
            entity.setAuthor(dto.getAuthor());
        }
        return entity;
    }

}
9

私の意見では、3番目のソリューションが最適です。はい、各エンティティに対して2つの新しい変換クラスを作成する必要がありますが、テストの時間になっても頭痛の種はあまりありません。最初に書くコードの量を減らし、そのコードのテストと保守に関してより多くのコードを書くソリューションを決して選択すべきではありません。

6
Petar Petrov

魔法のマッピングライブラリや外部コンバータークラスを使用せず、各エンティティから必要な各DTOにconvertメソッドを持つ小さなBeanを追加するだけでした。その理由は、マッピングが次のとおりだったためです。

eitherばかげてシンプルで、おそらく小さなユーティリティメソッドを使用して、あるフィールドから別のフィールドに値をコピーするだけです。

orは非常に複雑であり、カスタムコードを汎用マッピングライブラリに書き込むのは、そのコードを書き出すよりも複雑です。これは、たとえばクライアントがJSONを送信できるが、内部でこれがエンティティに変換され、クライアントがこれらのエンティティの親オブジェクトを再度取得すると、JSONに変換される場合です。

つまり、エンティティのコレクションで.map(converter::convert)を呼び出すだけで、DTOのストリームを取得できます。

すべてを1つのクラスにまとめることはスケーラブルですか?汎用マッパーを使用している場合でも、このマッピングのカスタム構成はどこかに保存する必要があります。いくつかのケースを除いて、コードは一般に非常に単純なので、このクラスが複雑に爆発することをあまり心配していません。私はまた、数十のエンティティを持つことを期待していませんが、もしそうなら、サブドメインごとのクラスにこれらのコンバーターをグループ化するかもしれません。

エンティティとDTOに基本クラスを追加して、汎用コンバーターインターフェイスを記述し、クラスごとに実装できるようにする必要はありません(まだ?)。