This commit is contained in:
Peter Straßer
2021-04-12 22:14:21 +02:00
parent 1cb5bc49ba
commit b65a635905
19 changed files with 88 additions and 54 deletions

View File

@@ -15,11 +15,12 @@ Most of the information on how to design in this way are from the following book
To run this application you need to first run a
```cmd
mvn clean install
mvn clean
```
in the root directory of this project. This will download all required dependecies and bundle your
project properly.
project properly. It will also auto generate some mapping classes with
the [mapstruct](https://mapstruct.org/) plugin.
The database for this application is a mongo db. You either need to have one running on your system,
or you can alternatively start the MongoDB in docker with the included docker-compose file.
@@ -50,7 +51,42 @@ way.
### Application architecture
![img_3.jpg](documentation/dependency_diagram.jpg)
This application tries to follow the design princples as described in Clean Architecture. To achieve
key characteristics of a good architecture (Single Responsibility, Flexibility, Testability, ... ),
one very defining lession of Robert C. Martins book is that the business logik should be at the core
of the system.
![circles](documentation/circles.png)
This diagram shows where dependencies in a system should point to achieve this. How to implement
this however, feels very abstract.
Alistair Cockburn took this concept and
definied [hexagonal architecture](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)).
Alternatively this style is also known as the ports and adapters architecture. Perhaps more fitting,
but certainly not as exciting sounding as a hexagon.
![hexagonal](documentation/Hexagonal.png)
In the middle lives the **Domain**. This is where all the business logik is contained. Ideally this
code is free of any framework specific annotations. This is especially great for unit testing your
logik. No spring context, that has to boot, or any services that need to be mocked. Just plain
normal Java, which is easy to understand and fast to execute.
Ecapsulating these Domain models are the **Use Cases**. These represent the features that our
application provides. They receive commands and query / write the data to the adapters.
Between the Use Cases and the **Adapters** are the **Ports**. Especially the output ports are
important to control the direction of dependencies. In Java these represent interfaces. The output
adapters implement the output ports and therefore have to follow the contract, that the core of the
application defines for them. The input adapters only interact with the input ports and never with
the implementing serivces directly.
This clear separation of concerns greatly increases flexibiliy. If you wanted to switch from an SQL
to NoSQL DB, you'd just have to rewrite your Persistence Adapter, but the rest of the application
should not be affected by your change.
![dependency diagram](documentation/dependencies.png)
### Advantages of separated models on each layer in this example
@@ -58,7 +94,10 @@ way.
the concern in what way to display the data to the client.
- The customer can be persisted different from the domain models structure. The layout in this
example has no benefit, but assume structuring your data in this way would give you a much needed
performance boost. This way the concern on how to handle data persistence is independent from the
business layer and can be handled by the persistence module.
- The AddressType properties can be annotated with web specific annotiations to control
serialization, without cluttering the domain.
- The customer can be persisted different from the domain models structure. This way the concern on
how to handle data persistence is independent from the business layer and can be handled by the
persistence module.

View File

@@ -1,14 +1,14 @@
package de.strasser.peter.hexagonal.addressvalidation;
import de.strasser.peter.hexagonal.application.customer.domain.Address;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorPort;
import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
class AddressValidator implements AddressValidatorAdapter {
class AddressValidator implements AddressValidatorPort {
@Override
public Address validate(ValidateAddressCommand validateAddressCommand) throws InvalidAddressExc {

View File

@@ -2,8 +2,8 @@ package de.strasser.peter.hexagonal.persistence;
import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import de.strasser.peter.hexagonal.application.customer.port.in.QueryAllCustomersCRUD;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerPort;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerPort;
import de.strasser.peter.hexagonal.persistence.errors.CustomerDoesNotExistExc;
import de.strasser.peter.hexagonal.persistence.mapper.CustomerMapper;
import de.strasser.peter.hexagonal.persistence.model.CustomerEntity;
@@ -19,7 +19,7 @@ import java.util.List;
@Repository
@RequiredArgsConstructor
public class CustomerDao
implements SaveCustomerAdapter, LoadCustomerAdapter, QueryAllCustomersCRUD {
implements SaveCustomerPort, LoadCustomerPort, QueryAllCustomersCRUD {
private final CustomerRepository customerRepository;
private final CustomerMapper customerMapper;

View File

@@ -0,0 +1,12 @@
package de.strasser.peter.hexagonal.web.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
public enum AddressTypeResponse {
@JsonProperty("default")
DEFAULT,
@JsonProperty("shipping")
SHIPPING,
@JsonProperty("billing")
BILLING;
}

View File

@@ -16,6 +16,6 @@ public class CustomerResponse {
private BigInteger id;
private String name;
private LocalDate birthday;
private Map<Address.AddressType, AddressResponse> addresses;
private Map<AddressTypeResponse, AddressResponse> addresses;
private int age;
}

View File

@@ -1,14 +0,0 @@
package de.strasser.peter.hexagonal.web.mapper;
import de.strasser.peter.hexagonal.application.customer.domain.Address;
import de.strasser.peter.hexagonal.web.dto.response.AddressResponse;
import org.mapstruct.Mapper;
import java.util.Map;
@Mapper
public interface AddressWebMapper {
AddressResponse toResponse(Address address);
Map<Address.AddressType, AddressResponse> toResponse(Map<Address.AddressType, Address> addresses);
}

View File

@@ -1,13 +1,10 @@
package de.strasser.peter.hexagonal.web.mapper;
import de.strasser.peter.hexagonal.application.customer.domain.Address;
import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import de.strasser.peter.hexagonal.web.dto.response.AddressResponse;
import de.strasser.peter.hexagonal.web.dto.response.CustomerResponse;
import org.mapstruct.Mapper;
import java.util.List;
import java.util.Map;
@Mapper
public interface CustomerWebMapper {

View File

@@ -3,6 +3,6 @@ package de.strasser.peter.hexagonal.application.customer.port.out;
import de.strasser.peter.hexagonal.application.customer.domain.Address;
import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand;
public interface AddressValidatorAdapter {
public interface AddressValidatorPort {
Address validate(ValidateAddressCommand validateAddressCommand);
}

View File

@@ -4,6 +4,6 @@ import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import java.math.BigInteger;
public interface LoadCustomerAdapter {
public interface LoadCustomerPort {
Customer findById(BigInteger id);
}

View File

@@ -2,6 +2,6 @@ package de.strasser.peter.hexagonal.application.customer.port.out;
import de.strasser.peter.hexagonal.application.customer.domain.Customer;
public interface SaveCustomerAdapter {
public interface SaveCustomerPort {
void upsert(Customer customer);
}

View File

@@ -5,9 +5,9 @@ import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import de.strasser.peter.hexagonal.application.customer.mapper.AddAddressMapper;
import de.strasser.peter.hexagonal.application.customer.port.in.AddAddressUseCase;
import de.strasser.peter.hexagonal.application.customer.port.in.commands.AddAddressCommand;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorPort;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerPort;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerPort;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -22,9 +22,9 @@ import java.util.Map;
@Service
@RequiredArgsConstructor
class AddressService implements AddAddressUseCase {
private final SaveCustomerAdapter saveCustomerAdapter;
private final AddressValidatorAdapter addressValidatorAdapter;
private final LoadCustomerAdapter loadCustomerAdapter;
private final SaveCustomerPort saveCustomerAdapter;
private final AddressValidatorPort addressValidatorAdapter;
private final LoadCustomerPort loadCustomerAdapter;
private final AddAddressMapper addAddressMapper;
@Override

View File

@@ -3,7 +3,7 @@ package de.strasser.peter.hexagonal.application.customer.service;
import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import de.strasser.peter.hexagonal.application.customer.port.in.RegisterCustomerUseCase;
import de.strasser.peter.hexagonal.application.customer.port.in.commands.RegisterCustomerCommand;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerPort;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -16,7 +16,7 @@ import javax.validation.Valid;
@Validated
@RequiredArgsConstructor
class RegisterCustomerService implements RegisterCustomerUseCase {
private final SaveCustomerAdapter saveUser;
private final SaveCustomerPort saveUser;
@Override
public void register(@Valid RegisterCustomerCommand registerCmd) {

View File

@@ -4,9 +4,9 @@ import de.strasser.peter.hexagonal.application.customer.domain.Address;
import de.strasser.peter.hexagonal.application.customer.domain.Customer;
import de.strasser.peter.hexagonal.application.customer.mapper.AddAddressMapper;
import de.strasser.peter.hexagonal.application.customer.port.in.commands.AddAddressCommand;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorPort;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerPort;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerPort;
import de.strasser.peter.hexagonal.application.customer.port.out.commands.ValidateAddressCommand;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -29,9 +29,9 @@ import static org.mockito.BDDMockito.then;
class AddressServiceTest {
@Autowired private AddressService sut;
@MockBean private SaveCustomerAdapter saveCustomerAdapterMock;
@MockBean private AddressValidatorAdapter addressValidatorAdapterMock;
@MockBean private LoadCustomerAdapter loadCustomerAdapterMock;
@MockBean private SaveCustomerPort saveCustomerAdapterMock;
@MockBean private AddressValidatorPort addressValidatorAdapterMock;
@MockBean private LoadCustomerPort loadCustomerAdapterMock;
@SpyBean private AddAddressMapper addAddressMapperMock;
@Test

View File

@@ -2,9 +2,9 @@ package de.strasser.peter.hexagonal.application.customer.service;
import de.strasser.peter.hexagonal.application.customer.mapper.AddAddressMapper;
import de.strasser.peter.hexagonal.application.customer.port.in.commands.RegisterCustomerCommand;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerAdapter;
import de.strasser.peter.hexagonal.application.customer.port.out.AddressValidatorPort;
import de.strasser.peter.hexagonal.application.customer.port.out.LoadCustomerPort;
import de.strasser.peter.hexagonal.application.customer.port.out.SaveCustomerPort;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,11 +25,11 @@ class RegisterCustomerServiceTest {
private RegisterCustomerService sut;
@MockBean
private SaveCustomerAdapter saveCustomerAdapterMock;
private SaveCustomerPort saveCustomerAdapterMock;
@MockBean
private AddressValidatorAdapter addressValidatorAdapterMock;
private AddressValidatorPort addressValidatorAdapterMock;
@MockBean
private LoadCustomerAdapter loadCustomerAdapterMock;
private LoadCustomerPort loadCustomerAdapterMock;
@MockBean
private AddAddressMapper addAddressMapperMock;

BIN
documentation/Hexagonal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
documentation/circles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1 +1 @@
<mxfile host="app.diagrams.net" modified="2021-04-11T15:02:04.080Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" etag="5IbEkqFB05nkIMoXACCY" version="14.4.9" type="device"><diagram id="gx9Nw2zk0I0ZlYx_-rDY" name="Page-1">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</diagram></mxfile>
<mxfile host="app.diagrams.net" modified="2021-04-12T20:12:54.578Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" etag="Mklv7aI7NHlyKOI3eBcS" version="14.5.10" type="device"><diagram id="rH_L7xBtymPGQX_kqgda" name="Page-1">7V1tk5s2EP41nmk/+AYhJPDHO1+SttPMXHOTpvnIgWzTw+ACPvvy6ysZhEHCMXDixYmdzBgkEPKzj1ar1S43gfP1/kNkb1YfQ5f4E11z9xN4P9F1XdM0+sVKXtMSAKCZliwjz83KjgWP3jeSFWY3LreeS+LShUkY+om3KRc6YRAQJymV2VEU7sqXLUK//NSNvSRSwaNj+3LpF89NVmmppZvH8t+It1zxJwM8S2vWNr84+yXxynbDXaEIvpvAeRSGSXq03s+Jz9DjuKT3vT9Rm3csIkFS54b40xf0V/T7VzJb/PGvdufgj2Q3NbPOvdj+NvvFWW+TVw7BbuUl5HFjO+x8R+U8gXerZO3TM0AP42eSOKvsZBEGyXt77flM3vNw7Tm0rUc7iOnXx0d2gef789APo0PbcIHYP9ZKEoXPpFCDD5+syUJ5+qHlWcdJlJD9SUhADjSlKAnXJIle6SWcn5xlr8L57ihqkHN4VZCzibJCO+PXMm/8KAJ6kEmhgURQDYlQKm3Y4Yrs7WUYUDA2JPLo80l0LH3gRRSAuzMyXHh7wgffQabZoaFJAi7Lz7WJtXAq5edY5GmhRk6GVpYTNJAkJ1wlJmPWlZiAJKX5Nk5o41HT8WP73pKK8N4nC9rrOwaUR/XPbVachBtRBgoQxUYZUWRUMF+vQNTqClBdAvQTWXox5S8HdjrRsc8geqIY4yU7+hyTuR2TppBHYWInHhs491MsUVwFYQXFYlTBa1QRtit4oQTvrevS/xGJ47ej2AuIpjY0iIYE4qP9Qjg/b117kzQf/yKMdrxJDZmDUm48wSoA3sKt2XtjWl3NixL2f4a2y7F/CKNErRYYAujhGY6r1ATTEX/Tacq1k/CtSHehKDAsw2hVmXG9wghkTSFhRgL3li1Q6Jnj23HsOWXQyN5L/mGI3aDs7GuGHzu+3xdPXvlJQHtfuImdfi3WHW87nL1WScTZRi9M8RTEQ1xplSQIh/60cBs55DugGNnKKLGjJUnOmQKytAvSRBXC5GUR8SnZXsr9rZJw9oSH0KO/5LgmEGxNBASSpL8zu6u43hIagvBMQykOUkOUFPZr4bINuyA+3WEIzLK2RqV1ID1IWzyyOcf0DQSX1bEigoNWBAcXRXA4KMEhKPMStyS4bpYJjsW1QUcEN2EfBJdnwg8kuaWL38ziiOefPt+PbCLUDfNGowvh/INLwEFz6HmRryhl+6IpksptCIHMqAKrWa9QKTQhQNGAyLXtBWpYpNfUsMaQGtZCZS5JXqi6GnYm2CKGyLYTGlaVEtQVzvIqODgKM7Y2B9GQHDRNVNb9rTkI0I2FgDnDCFP+QcHYBOBGB1DXoAERmmFe3RdB5Vn6SlAEfy6CHoyecmOopimqjIemAh62nHjb8FcFD+vNwbX5OhgPhWW6aV4yDy11+lDFsvyy9CEelIeC0Wi21YeWVeagROiOOcjdC1cOtjEaT6xSL4yDMw19v6GuOah3xcF2/vdxLJ7r6sFBOYhnwpoXtuSg6NHBes8clLfce+HguFzdYNBJNQ9h4faYyIG6ZAICmYyeJ1Xu+CnGG4XBwltuo9ShLDKLh4glEf3+FrLGS0FiefnDsbBplBiPXHIofQ73q3bKQqjflCcSnWuDAn9MiGQCiZsa6mJAVJo3rVwOJePmOM8MuvOl1VQHqd4YSh1MhVGc7/U2VQdTIXAD1Jxbmm596cLilNP65JacuOc862GrzFBobLVywoGLHhHWmCZI0NbaAkKwXl3PR9MRAYDVbEhIN/QzJhQafz/hmDAHHRMQqRoUulneLoE1zcb+R8UM9DEqOguLu2DXUO1RkbJyqFGBRNtJ3IuuvS4XmAd7XpcbVXs1aWJBvLEDnlpQjnulS6w8/YA+tXihnJVQ3VwWHH6iDWEU9B39IqSDWFxG59JBultmVe1k4IkOgZYiXDgsgF3AEP+3Zal1d06ak3TLhsDy6RfaYdojjX/9emhCY9He0zTriV0I9M2+WJE2ymqCMFrbfqFul6HBKlme0qHGJwkV9pT2yfGCpXwnlVQyzZbPrC5fQfM6L3APomOVGu/LoYYu3IN4QdvirQYkrd2FkVt+Yn7jk+08L6NwG7hTAQ2d4cyA0NnK7HCAOCauF298NgWwi73A9/iTFn5oJ8LjOdqc0Q8kiln2TuCQOgPnKjdlchMlcR0goxwg5yaEal33hmmij4QWoIkOCDz4RDL7wab7PuRIrb3vBRIPL1W+n9osXfSRRC+e0zjRsWtrqzK/rl84q9wVHVtbi4zDt4cf6b8QlvZ8nWw6mmwO2i3nf19zTgeDBQ++NEHNvBhPfug8UyxcO17l/gNa/95jj828Dvm2mj5RuFM0Qm9H7SCE80GBAKvxbEwFH7jVNuMPiKvomhl/qjwbqFm4/miIOY7oGL0mMfX+iCnsqVitoxfQmYa6JmazMH01xPxh0qatmsQ8n/OkiphA3DbkOcctdkisMy2p2iHRxLiyPtKkUbO8gA6If8np1HWJfz6PRRnxBT3adg9EDCeTGuqK9b3kTqNmWQijUffjYL1Zk/XnAytVsV60j1sre+tMQ12bIVWuwK542dKkGG/Eb11tXCPiVxUxxff+tH2TBZidaagrdQzL6vjsqy9gH+qb//irx/yid9CxHKjc9bu8egF6dC/7wvopoFXsNwxB3uF9rLjyfZXXF8VUQSW7o6+von0borIfteN3P/Wypzv2l0Nh2Us41CuA+5DH+N4RjGVv1diTzPoQ1Biz0LDsYukp/mEIe2T4AAksuw76m2SHgLzLWZieHv/sRbpqPf71EPjufw==</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB