it-swarm.com.de

Richtiges Konvertieren von Domänenentitäten in DTOs unter Berücksichtigung von Skalierbarkeit und Testbarkeit

Ich habe mehrere Artikel und Stackoverflow-Posts zum Konvertieren von Domänenobjekten in DTOs gelesen und sie in meinem Code ausprobiert. Wenn es um Tests und Skalierbarkeit geht, habe ich immer einige Probleme. Ich kenne die folgenden drei möglichen Lösungen zum Konvertieren von Domänenobjekten in DTOs. Meistens benutze ich Spring.

Lösung 1: Private Methode in der Service-Schicht zum Konvertieren

Die erste mögliche Lösung besteht darin, eine kleine "Hilfs" -Methode im Service-Layer-Code zu erstellen, die das abgerufene Datenbankobjekt in mein DTO-Objekt konvertiert.

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

Vorteile:

  • einfach zu implementieren
  • keine zusätzliche Klasse für die Konvertierung erforderlich -> Projekt sprengt nicht mit Entitäten

Nachteile:

  • probleme beim Testen, da new SomeEntity() in der privatisierten Methode verwendet wird und wenn das Objekt tief verschachtelt ist, muss ich ein adäquates Ergebnis meiner when(someDao.findById(id)).thenReturn(alsoDeeplyNestedObject) liefern, um NullPointers zu vermeiden, wenn die Konvertierung auch das auflöst verschachtelte Struktur

Lösung 2: Zusätzlicher Konstruktor im DTO zum Konvertieren der Domänenentität in DTO

Meine zweite Lösung wäre, meiner DTO-Entität einen zusätzlichen Konstruktor hinzuzufügen, um das Objekt im Konstruktor zu konvertieren.

public class SomeDto {

 // ... some attributes

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

}

Vorteile:

  • keine zusätzliche Klasse zum Konvertieren erforderlich
  • konvertierung in der DTO-Entität versteckt -> Service-Code ist kleiner

Nachteile:

  • verwendung von new SomeDto() im Service-Code und dafür muss ich die richtige geschachtelte Objektstruktur als Ergebnis meiner someDao-Verspottung angeben.

Lösung 3: Verwenden von Spring's Converter oder einer anderen ausgelagerten Bean für diese Konvertierung

Wenn ich kürzlich gesehen habe, dass Spring aus Konvertierungsgründen eine Klasse anbietet: Converter<S, T>, Steht diese Lösung jedoch für jede externalisierte Klasse, die die Konvertierung durchführt. Mit dieser Lösung injiziere ich den Konverter in meinen Dienstcode und rufe ihn auf, wenn ich die Domänenentität in mein DTO konvertieren möchte.

Vorteile:

  • einfach zu testen, da ich das Ergebnis während meines Testfalls verspotten kann
  • aufgabentrennung -> Eine spezielle Klasse erledigt die Arbeit

Nachteile:

  • "skaliert" nicht so sehr, wie mein Domain-Modell wächst. Bei vielen Entitäten muss ich für jede neue Entität zwei Konverter erstellen (-> DTO-Entität und -Entität in DTO konvertieren)

Haben Sie weitere Lösungen für mein Problem und wie gehen Sie damit um? Erstellen Sie für jedes neue Domänenobjekt einen neuen Konverter und können mit der Anzahl der Klassen im Projekt "leben"?

Danke im Voraus!

19
rieckpil

Lösung 1: Private Methode in der Serviceebene für die Konvertierung

Ich denke, Lösung 1 wird nicht gut funktionieren, da Ihre DTOs domänenorientiert und nicht serviceorientiert sind. Daher ist es wahrscheinlich, dass sie in verschiedenen Diensten verwendet werden. Eine Zuordnungsmethode gehört also nicht zu einem Dienst und sollte daher nicht in einem Dienst implementiert werden. Wie würden Sie die Zuordnungsmethode in einem anderen Dienst wiederverwenden?

Die 1. Lösung würde gut funktionieren, wenn Sie dedizierte DTOs pro Dienstmethode verwenden. Aber dazu am Ende mehr.

Lösung 2: Zusätzlicher Konstruktor im DTO zum Konvertieren der Domänenentität in DTO

Im Allgemeinen eine gute Option, da Sie das DTO als Adapter für die Entität sehen können. Mit anderen Worten: Das DTO ist eine weitere Darstellung einer Entität. Solche Entwürfe umschließen häufig das Quellobjekt und bieten Methoden, mit denen Sie eine andere Sicht auf das umschlossene Objekt erhalten.

Ein DTO ist jedoch ein Datenobjekt transfer, sodass es früher oder später serialisiert und über ein Netzwerk gesendet werden kann, z. Verwenden von Fernsteuerungsfunktionen der Feder . In diesem Fall muss der Client, der dieses DTO empfängt, es deserialisieren und benötigt daher die Entitätsklassen in seinem Klassenpfad, auch wenn er nur die Schnittstelle des DTO verwendet.

Lösung 3: Verwenden von Spring's Converter oder einer anderen ausgelagerten Bean für diese Konvertierung

Lösung 3 ist die Lösung, die ich auch bevorzugen würde. Ich würde jedoch eine Mapper<S,T> - Schnittstelle erstellen, die für die Zuordnung von Quelle zu Ziel und umgekehrt verantwortlich ist. Z.B.

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

Die Implementierung kann mit einem Mapping-Framework wie modelmapper erfolgen.


Sie sagten auch, dass ein Konverter für jede Entität

"skaliert" nicht so sehr, wie mein Domain-Modell wächst. Bei vielen Entitäten muss ich für jede neue Entität zwei Konverter erstellen (-> DTO-Entität und -Entität in DTO konvertieren)

Ich bezweifle, dass Sie nur 2 Konverter oder einen Mapper für ein DTO erstellen müssen, da Ihr DTO domänenorientiert ist.

Sobald Sie anfangen, es in einem anderen Dienst zu verwenden, werden Sie erkennen, dass der andere Dienst normalerweise alle Werte zurückgeben sollte oder nicht kann, die der erste Dienst tut. Sie werden beginnen, einen anderen Mapper oder Konverter für jeden anderen Service zu implementieren.

Diese Antwort würde zu lang werden, wenn ich mit Vor- und Nachteilen dedizierter oder gemeinsam genutzter DTOs beginne. Daher kann ich Sie nur bitten, meinen Blog zu lesen Vor- und Nachteile von Service-Layer-Designs .

14
René Link

Ich mag die dritte Lösung aus der akzeptierten Antwort.

Lösung 3: Verwenden von Spring's Converter oder einer anderen ausgelagerten Bean für diese Konvertierung

Und so erstelle ich DtoConverter:

BaseEntity-Klassenmarkierung:

public abstract class BaseEntity implements Serializable {
}

AbstractDto-Klassenmarkierung:

public class AbstractDto {
}

GenericConverter-Schnittstelle:

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-Schnittstelle:

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

Implementierung der CommentConveter-Klasse:

@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

Meiner Meinung nach ist die dritte Lösung die beste. Ja, für jede Entität müssen Sie zwei neue Konvertierungsklassen erstellen, aber wenn Sie Zeit zum Testen haben, werden Sie nicht viele Kopfschmerzen haben. Sie sollten niemals die Lösung wählen, die dazu führt, dass Sie am Anfang weniger Code schreiben und dann viel mehr schreiben, wenn Sie diesen Code testen und warten.

6
Petar Petrov

Ich habe KEINE magische Zuordnungsbibliothek oder externe Konvertierungsklasse verwendet, sondern nur eine eigene kleine Bean hinzugefügt, die convert -Methoden von jeder Entität zu jedem von mir benötigten DTO enthält. Der Grund ist, dass das Mapping war:

entweder dumm einfach und ich würde nur einige Werte von einem Feld in ein anderes kopieren, vielleicht mit einer kleinen Utility-Methode,

oder war ziemlich komplex und es wäre komplizierter, die benutzerdefinierten Parameter in eine generische Zuordnungsbibliothek zu schreiben, als nur diesen Code zu schreiben. Dies ist beispielsweise der Fall, wenn der Client JSON senden kann, dies jedoch unter der Haube in Entitäten umgewandelt wird. Wenn der Client das übergeordnete Objekt dieser Entitäten erneut abruft, wird es wieder in JSON konvertiert.

Dies bedeutet, dass ich .map(converter::convert) für jede Sammlung von Entitäten aufrufen kann, um einen Stream meiner DTOs zurückzugewinnen.

Ist es skalierbar, alles in einer Klasse zu haben? Nun, die benutzerdefinierte Konfiguration für dieses Mapping müsste irgendwo gespeichert werden, selbst wenn ein generischer Mapper verwendet wird. Der Code ist im Allgemeinen extrem einfach, mit Ausnahme einiger weniger Fälle. Ich mache mir also keine Sorgen, dass diese Klasse in ihrer Komplexität explodiert. Ich rechne auch nicht mit Dutzenden weiterer Entitäten, aber wenn ich das täte, könnte ich diese Konverter in einer Klasse pro Unterdomäne gruppieren.

Das Hinzufügen einer Basisklasse zu meinen Entitäten und DTOs, damit ich eine generische Konverter-Schnittstelle schreiben und diese pro Klasse implementieren kann, ist (noch?) Auch für mich nicht erforderlich.