Kontakt
stefan.bente[at]th-koeln.de
+49 2261 8196 6367
Discord Server
Prof. Bente Personal Zoom
Adresse
Steinmüllerallee 4
51643 Gummersbach
Gebäude LC4
Raum 1708 (Wegbeschreibung)
Sprechstunde nach Vereinbarung
Terminanfrage: calendly.com Wenn Sie dieses Tool nicht nutzen wollen, schicken Sie eine Mail und ich weise Ihnen einen Termin zu.

Anti-Patterns bei "DDD mit Spring JPA"

Im ST2-Praktikum begegnen uns die nachfolgenden Anti-Patterns und Fehler häufig. Will man eine DDD-Architektur mit Spring JPA umsetzen, dann sollte man diese vermeiden. Wenn Sie nicht so genau wissen, was mit “Anti-Patterns” gemeint ist, dann schauen Sie sich das Video dazu an. Die nachfolgende Liste kann Ihnen helfen, oft auftretende Fehler zu vermeiden. Die nachfolgenden Patterns führen nicht zu Fehlern im Code, aber zu schlechtem Code. Die Anti-Patterns haben keine Prioritätsreihenfolge, das ist eine ungeordnete Sammlung.

Video(s) hierzu

UUIDs statt Entity-Referenzen

Einige Male haben wir folgenden Code gesehen:

@Entity
public class Planet {
    @Id
    @GeneratedValue
    private UUID id;
    private UUID northNeighbourId;
    private UUID eastNeighbourId;
    private UUID southNeighbourId;
    private UUID westNeighbourId;
    //...
}

Nur weil das Certification-Interface UUIDs verwendet, ist das noch lange kein Grund, das im Entity auch zu machen! So eine Programmierung führt dazu, dass man im ApplicationService dann so in der Art programmieren muss (Pseudocode - habe es nicht auf Lauffähigkeit getestet):

    public Planet findPlanet( UUID id ) {
        Planet planet = planetRepository.findById( id ).orElseThrow( () -> new PlanetException( "..." ) );
        UUID northNeighbourId = planet.northNeighbourId();
        Planet northNeighbour = planetRepository.findById( northNeighbourId ).
                                    orElseThrow( () -> new PlanetException( "..." ) );
        // etc.                            
    }

Statt dessen machen Sie IMMER Beziehungen zu anderen Entities:

@Entity
public class Planet {
    @Id
    @GeneratedValue
    private UUID id;
    @OneToOne
    private Planet northNeighbour;
    @OneToOne
    private Planet eastNeighbour;
    @OneToOne
    private Planet southNeighbour;
    @OneToOne
    private Planet westNeighbour;
    //...
}

Wenn Sie das machen, dann werden beim Fetch des Entities aus der DB die Nachbarschaftsbeziehungen mit geholt. Müssen Sie sich also gar nicht drum kümmern! Sie können in den Methoden von Planet direkt auf die Nachbarn zugreifen.

Code Copy-Pasting

Häufig auch das Copy-Pasting von Code, der eigentlich identisch ist. Vielleicht so in der Art:

    public void setNorthNeighbourRelation( Planet planet ) {
        //
        // 
        // ... irgendwelche 20 Zeilen von Prüfungen und setzen der Beziehung ... 
        //
    }
    
    public void setEastNeighbourRelation( Planet planet ) {
        //
        // 
        // ... derselbe Code, nur "north" durch "east" ersetzt ... 
        //
    }
    
    // ... und dann nochmal genauso für "south" und "west"

Es leuchtet vermutlich ein, warum das ein ganz schlechter Stil ist: Wenn Sie einen Bug einbauen und den dann fixen müssen, dann müssen Sie ihn 4x fixen. Wie hoch ist die Chance, dass man es 3x macht und 1x vergisst?

Prozedurales statt objekt-orientiertes Programmieren

Häufiger konnte man auch folgendes Anti-Pattern, das im Kern ein prozedurales statt objekt-orientiertes Programmieren ist. Im ApplicationService von Robot gab es dann so in etwa die folgenden Methoden:

    public void move( Direction direction, Robot robot ) {
        // ... und dann wurde der Robot mittels Repo aus der DB geholt, auf einen neuen Planeten bewegt,  
        // und dann mittels Repo wieder abgespeichert.
    }

Damit ist die Business-Logik für den Robot da, wo sie nicht hingehört: nämlich außerhalb des Robots. Viel besser (und objektorientiert …) ist es, diese Logik im Robot selbst zu implementieren. Ganz grob etwa so:

@Service
public class RobotApplicationService {
    //...
    public void executeCommand( Command command ) {
        //...
        Robot robot = robotRepository.findById( command.getRobotId() ).
                            orElseThrow( () -> new RobotException( "..." );
        if ( command.isMove() ) {
            robot.move( command.getDirection() );
        }
    }
    //...
}

@Entity
public class Robot {
    //...
    public void move( Direction direction ) {
        // Move-Logik ist jetzt da, wo sie hingehört: INNERHALB des Robots.
        //...
    }
    //...
}

Business-Logik im ApplicationService, statt in der Domain (also in den Entities)

Wenn man das vorige Anti-Pattern “konsequent umsetzt” (Achtung, Ironie!), dann ist am Ende die gesamte Business-Logik in einer Klasse, nämlich dem ApplicationService. Die Entities sind dann nur noch reine “CRUD-Container”. Damit hat man jetzt Art von Reuse und guter Wartbarkeit wirkungsvoll verhindert :-(.

Value Objects mit public Settern

Thematischer Schwenk: Value Objects (… @Embeddable-Klassen …) sind per Definition immutable. Sie haben also keine public Setter! Sonst kann man sie von außen ändern.

Value Objects ohne passende equals( Object obj ) Implementation

Zwei Value Objects sind per Definition gleich, wenn ihre Attributwerte gleich sind. Daraus folgt zwingend, dass sie eine Implementation für die Methode equals( Object obj ) haben, die alle Attributwerte einbezieht und vergleicht. Wenn man das nicht implementiert, wird nur die Java-Object-ID verglichen - und dann sind zwei verschiedene Instanzen immer ungleich.

Repositories in den Entities selbst

Man könnte versucht sein, im Entity selbst ein Repository als Membervariable (oder Methodenparameter) zu haben - dann könnte man ja im Entity direkt speichern, oder andere Entities holen.

Das sollte man aber vermeiden, denn der Domain Layer ist technologie-agnostisch. Man bräuchte ja keine relationale Datenbank zu nutzen, sondern könnte Objekte anders persistieren. Man sollte also das Lifecycle-Management einer DB außerhalb des Domain Layers machen - z.B. in einem ApplicationService.

IDs vom Typ Long statt UUID

In ganz vielen Web-Tutorials zu JPA findet man folgende Implementation einer Entity-ID:

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

Long-IDs haben aber große Nachteile, insbesondere dass sie Sequenzen bilden und damit “erratbar” werden. Daher: Nehmen Sie im Regelfall immer UUIDs als Entity-ID. Also besser:

    @Id
    @GeneratedValue
    private UUID id;

System.out.println statt Verwendung des Loggers

System.out.println ist praktisch, um sich ohne Debugger den “Trace” einer Anwendung ausgeben zu lassen. Für das Praktikum ist das egal, aber in der Praxis ist es immer besser, stattdessen einen Logger zu benutzen. Der funktioniert auch im Produktiv-Environment (im Gegensatz zu System.out.println). In Spring ist das ganz einfach:

@Entity
public class Planet {
    //...
    @Transient    
    private Logger logger = LoggerFactory.getLogger( Planet.class );
    //...
    
    public void setNeighbour( Direction direction, Planet neighbour ) {
        logger.info( "Attempt to set neighbour " + neighbour + " at " + direction );
    }
}

Im Produktiv-Environment können Sie den Logging-Level dann je nach Bedarf hoch- oder runtersetzen.

Field-based Autowiring

Sie können eine Spring-gemanagedte Klasse (z.B. ein Repository oder einen Service) über @Autowired automatisch instanziieren lassen. Dafür gibt es mehrere grundsätzliche Wege. Beim Field-based Autowiring wird @Autowired direkt über der Variablendeklaration angegeben:

public class PlanetApplicationService {
    @Autowired
    private PlanetRepository planetRepository;
    //...
}

Damit kann man die Klasse PlanetApplicationService aber nicht ohne das Spring-Framework nutzen, z.B. wenn man mit einem Mock testen will. Besser ist Constructor-based Autowiring:

public class PlanetApplicationService {
    private PlanetRepository planetRepository;

    @Autowired
    public PlanetApplicationService( PlanetRepository planetRepository ) {
        this.planetRepository = planetRepository;
    }
    //...
}

Hier werden die Parameter des Konstruktors “autowired”. Man kann die Klasse dann auch ohne Spring instanziieren.