Hexagonal Architecture with Spring Boot example

This commit is contained in:
Sallo Szrajbman
2020-09-23 20:03:24 +01:00
parent 36920b7e24
commit 412b869003
13 changed files with 371 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baeldung</groupId>
<artifactId>hexagonal-architecture-example</artifactId>
<version>1.0</version>
<name>hexagonal-architecture-example</name>
<description>Project for hexagonal architecture example in java</description>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,26 @@
package com.baeldung.pattern.hexagonal.architecture;
import com.baeldung.pattern.hexagonal.architecture.adapters.persistence.CarRepository;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class HexagonalArchitectureApplication {
public static void main(String[] args) {
SpringApplication.run(HexagonalArchitectureApplication.class, args);
}
@Bean
public CommandLineRunner bootstrapData(CarRepository carRepository) {
return (args) -> {
Car car = new Car();
car.setId(1L);
car.setInUse(false);
carRepository.save(car);
};
}
}

View File

@@ -0,0 +1,28 @@
package com.baeldung.pattern.hexagonal.architecture.adapters.persistence;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.LoadCarPort;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.SaveCarPort;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class CarRepository implements LoadCarPort, SaveCarPort {
private SpringDataCarRepository springDataCarRepository;
public CarRepository(SpringDataCarRepository springDataCarRepository) {
this.springDataCarRepository = springDataCarRepository;
}
@Override
public Optional<Car> load(Long id) {
return springDataCarRepository.findById(id);
}
@Override
public void save(Car car) {
springDataCarRepository.save(car);
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.pattern.hexagonal.architecture.adapters.persistence;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import org.springframework.data.repository.CrudRepository;
public interface SpringDataCarRepository extends CrudRepository<Car, Long> {
}

View File

@@ -0,0 +1,31 @@
package com.baeldung.pattern.hexagonal.architecture.adapters.web;
import com.baeldung.pattern.hexagonal.architecture.application.port.incoming.RentCarUseCase;
import com.baeldung.pattern.hexagonal.architecture.application.port.incoming.ReturnRentalUseCase;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/car")
public class CarController {
private final RentCarUseCase rentCarUseCase;
private final ReturnRentalUseCase returnRentalUseCase;
public CarController(RentCarUseCase rentCarUseCase, ReturnRentalUseCase returnRentalUseCase){
this.rentCarUseCase = rentCarUseCase;
this.returnRentalUseCase = returnRentalUseCase;
}
@PostMapping(value = "/{id}/rent")
void rent(@PathVariable final Long id) {
rentCarUseCase.rent(id);
}
@PostMapping(value = "/{id}/return-rental")
void returnRental(@PathVariable final Long id) {
returnRentalUseCase.returnRental(id);
}
}

View File

@@ -0,0 +1,40 @@
package com.baeldung.pattern.hexagonal.architecture.application.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Car {
@Id
private Long id;
private Boolean inUse;
public boolean rent() {
if(this.inUse) {
return false;
} else {
this.inUse = true;
return true;
}
}
public boolean returnRental() {
if(this.inUse) {
this.inUse = false;
return true;
} else {
return false;
}
}
//getter and setters
}

View File

@@ -0,0 +1,5 @@
package com.baeldung.pattern.hexagonal.architecture.application.port.incoming;
public interface RentCarUseCase {
boolean rent(Long id);
}

View File

@@ -0,0 +1,5 @@
package com.baeldung.pattern.hexagonal.architecture.application.port.incoming;
public interface ReturnRentalUseCase {
boolean returnRental(Long id);
}

View File

@@ -0,0 +1,9 @@
package com.baeldung.pattern.hexagonal.architecture.application.port.outgoing;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import java.util.Optional;
public interface LoadCarPort {
Optional<Car> load(Long id);
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.pattern.hexagonal.architecture.application.port.outgoing;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
public interface SaveCarPort {
void save(Car car);
}

View File

@@ -0,0 +1,48 @@
package com.baeldung.pattern.hexagonal.architecture.application.services;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import com.baeldung.pattern.hexagonal.architecture.application.port.incoming.RentCarUseCase;
import com.baeldung.pattern.hexagonal.architecture.application.port.incoming.ReturnRentalUseCase;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.LoadCarPort;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.SaveCarPort;
import org.springframework.stereotype.Service;
import java.util.NoSuchElementException;
@Service
public class CarService implements RentCarUseCase, ReturnRentalUseCase {
private LoadCarPort loadCarPort;
private SaveCarPort saveCarPort;
public CarService(LoadCarPort loadCarPort, SaveCarPort saveCarPort) {
this.loadCarPort = loadCarPort;
this.saveCarPort = saveCarPort;
}
@Override
public boolean rent(Long id) {
Car car = loadCarPort.load(id).orElseThrow(NoSuchElementException::new);
boolean hasRent = car.rent();
if(hasRent) {
saveCarPort.save(car);
}
return hasRent;
}
@Override
public boolean returnRental(Long id) {
Car car = loadCarPort.load(id).orElseThrow(NoSuchElementException::new);
boolean hasReturned = car.returnRental();
if(hasReturned) {
saveCarPort.save(car);
}
return hasReturned;
}
}

View File

@@ -0,0 +1,92 @@
package com.baeldung.hexagonal.architecture;
import com.baeldung.pattern.hexagonal.architecture.application.domain.Car;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.LoadCarPort;
import com.baeldung.pattern.hexagonal.architecture.application.port.outgoing.SaveCarPort;
import com.baeldung.pattern.hexagonal.architecture.application.services.CarService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.NoSuchElementException;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
public class CarServiceUnitTest {
private LoadCarPort loadCarPort;
private SaveCarPort saveCarPort;
private CarService carService;
@BeforeEach
void setup() {
loadCarPort = mock(LoadCarPort.class);
saveCarPort = mock(SaveCarPort.class);
carService = new CarService(loadCarPort, saveCarPort);
}
@Test
void testRent_WhenCarNotFound_ThenShouldThrowNoSuchElementException() {
doReturn(Optional.empty()).when(loadCarPort).load(0L);
Assertions.assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> carService.rent(0L));
}
@Test
void testRent_WhenHasRentIsFalse_ThenShouldReturnFalse() {
Car car = mock(Car.class);
doReturn(false).when(car).rent();
doReturn(Optional.of(car)).when(loadCarPort).load(1L);
assertFalse(carService.rent(1L));
verify(saveCarPort, times(0)).save(car);
}
@Test
void testRent_WhenHasRentIsTrue_ThenShouldReturnTrue() {
Car car = mock(Car.class);
doReturn(true).when(car).rent();
doReturn(Optional.of(car)).when(loadCarPort).load(1L);
assertTrue(carService.rent(1L));
verify(saveCarPort, times(1)).save(car);
}
@Test
void testReturnRental_WhenCarNotFound_ThenShouldThrowNoSuchElementException() {
doReturn(Optional.empty()).when(loadCarPort).load(0L);
Assertions.assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> carService.returnRental(0L));
}
@Test
void testReturnRental_WhenHasReturnedIsFalse_ThenShouldReturnFalse() {
Car car = mock(Car.class);
doReturn(false).when(car).returnRental();
doReturn(Optional.of(car)).when(loadCarPort).load(1L);
assertFalse(carService.returnRental(1L));
verify(saveCarPort, times(0)).save(car);
}
@Test
void testReturnRental_WhenHasReturnedIsTrue_ThenShouldReturnTrue() {
Car car = mock(Car.class);
doReturn(true).when(car).returnRental();
doReturn(Optional.of(car)).when(loadCarPort).load(1L);
assertTrue(carService.returnRental(1L));
verify(saveCarPort, times(1)).save(car);
}
}