Skip to content
This repository was archived by the owner on Apr 9, 2025. It is now read-only.

Latest commit

 

History

History
494 lines (348 loc) · 16.5 KB

File metadata and controls

494 lines (348 loc) · 16.5 KB

Task Service - Business Logic Layer

1. Design of the Logic Layer and implementation of the first CRUD Use Cases

To get familiar with the structure of the Business Logic Layer will start with the implementation of very basic CRUD (Create, Read, Update, Delete) logic. We are going to create Transfer Objects, add Use Case Interfaces, implement and test them. If You are familiar with the TDD approach (or just want to try it out) you may try using TDD instead of creating all the tests at the end of this exercise.

1.1. Transfer Objects

During the previous exercise You created 3 entities: PersonEntity, TaskListEntity and TaskItemEntity. Now for each entity we will create an Entity Transfer Object (PersonEto, TaskListEto, TaskItemEto).

The Transfer Objects should be located in the following package:

com.capgemini.training.todo.task.common

Entity Transfer Objects will contain the same properties as the corresponding entity, but they should not include any relations. You may use Lombok to reduce the number of the boilerplate code. For instance, for PersonEntity the corresponding PersonEto should look as follows:

@Data
@Builder
public class PersonEto  {
    private final Long id;
    private final int version;
    private final String email;
}

We used here two Lombok annotations (@Data and @Builder). @Data is a convenient shortcut annotation that bundles the features of @ToString, @EqualsAndHashCode, @Getter / @Setter and @RequiredArgsConstructor together. The @Builder annotation introduces the Builder pattern into our POJO class.

It is considered to be a good practice to make Transfer Objects immutable (as in the example above). Since Java 14 we can create a record instead of a class:

@Builder
public record PersonEto (Long id,
                         int version,
                         String email) { }

Please implement the Transfer Objects for the remaining entities (TaskListEto and TaskItemEto) on your own.

1.2. Use Case interfaces

During this step we will start defining the API for our Business Logic Layer. Therefore, we will create the interfaces for our Use Cases. We are going to implement CRUD operations for two entities: Person and TaskItem. TaskList will be handled during the next steps.

The Use Case Interfaces should be located in the following package:

com.capgemini.training.todo.task.logic

For each entity we will create the two interfaces — one for Read and one for Write operations. For Person the interfaces should look as follows:

public interface ManagePersonUc {

    PersonEto savePerson(PersonEto personEto);

    void deletePerson(Long id);
}
public interface FindPersonUc {

    List<PersonEto> findAllPersons();

    Optional<PersonEto> findPerson(Long id);
}

Add similar Interfaces also for TaskItem.

1.3. Use Case implementation

As the next step you should implement the interfaces created in the previous step.

Please locate the Use Case implementations in following package:

com.capgemini.training.todo.task.logic.impl

Each of the created Use Cases has to be annotated with following annotations:

@Service
@Transactional

Each Use Case implementation should implement the corresponding interface. To implement the Use Case methods we need to inject the corresponding Repository and just delegate the functionality to the Repository methods. During the implementation we will need to map from the *Entity to *Eto or vice versa. For now, we will do it manually. If You would like to implement an automatic mapping using the Mapscruct framework then please follow the instructions from (Optional) Bean mapping using MapStruct afterwards.

Please check the following example:

@Service
@Transactional
public class ManagePersonUcImpl implements ManagePersonUc {

    private final PersonRepository personRepository;

    public ManagePersonUcImpl(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }

    @Override
    public PersonEto savePerson(PersonEto personEto) {

        PersonEntity personEntity = toPersonEntity(personEto);
        personEntity = personRepository.saveAndFlush(personEntity);
        return toPersonEto(personEntity);
    }

    @Override
    public void deletePerson(Long id) {
        // TODO Implement me!
    }

    private PersonEntity toPersonEntity(PersonEto personEto) {
        // TODO Implement me!
        return null;
    }

    private PersonEto toPersonEto(PersonEntity personEntity) {
        // TODO Implement me! Try using builder for the implementation
        return null;
    }
}

Please implement all the Use Cases.

1.4. Tests

In this part we’ll test the business logic layer of our Spring Boot application.

We can create spring-boot context aware test classes that will check our business logic implementation. For that we’ll create a test class that will be started without web environment context:

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
class ManagePersonUcTest {

    @Autowired
    private ManagePersonUc managePersonUc;

    @Test
    public void savePerson_shouldCreatePerson() {
        // ...
    }

    @Test
    public void savePerson_shouldUpdatePerson() {
        // ...
    }

    @Test
    public void deletePerson() {
        // ...
    }
}

In our case, the above integration tests will be relatively fast. However, to run such test we need to start the application context and the tests themselves will talk to the database, so in the real-live scenarios such tests can be very slow. Fortunately, we should already have our repositories tested, so to test our logic layer we can just mock them:

@ExtendWith(MockitoExtension.class)
class FindPersonUcImplTest {

    @InjectMocks
    private FindPersonUcImpl findPersonUc;

    @Mock
    private PersonRepository personRepository;

    @Test
    void findAllPersons() {
        // ...
    }

    @Test
    void findPerson() {
        // given
        PersonEntity personEntity; // Initilize me!
        when(personRepository.findById(1L)).thenReturn(Optional.of(personEntity));

        // when
        Optional<PersonEto> result = findPersonUc.findPerson(1L);

        // then
        assertThat(result).isPresent();
        // Check if returned person is as expected.
    }
}

Now we can implement some tests. Please provide some valid test cases for each method defined in our Use Cases — please test that each covered entity can be correctly created, updated, deleted and read.

2. Implementation of further Use Cases

Until now, we are only able to perform the CRUD operations on PersonEntity and TaskItemEntity. We cannot however, create TaskListEntities as well as fill the relationships between our entities. During this exercise we will add some more sophisticated logic:

  • Create Person with a TaskList

  • Create/Read/Update/Delete TaskList with its TaskItems

  • Create TaskList with the given name and given number of TaskItems (TaskItems should have some arbitrary data)

  • Finding TaskList by name

  • Finding overdue and uncompleted TaskItems

2.1. Transfer Object composition

To be able to create Person with a TaskList and/or TaskList with its TaskItems we have to create Composite Transfer Objects containing all the necessary data. Therefore, we will create:

  • PersonCto which will reference to PersonEto and TaskListEto

  • TaskListCto which will reference to TaskListEto and the list of TaskItemEto

PersonCto should look like this:

@Data
@Builder
public class PersonCto  {
    private final PersonEto personEto;
    private final TaskListEto taskListEto;
}

or like this:

@Builder
public record PersonCto (PersonEto personEto,
                         TaskListEto taskListEto) { }

Please add TaskListCto on Your own.

2.2. Use Case interfaces

Note
It may be more convenient to implement the missing logic incrementally — by adding the new method to the interface, implementing and testing it (instead of adapting all interfaces at once and implementing all of them afterwards).
Note
You can implement the missing logic in any order You would like, please try to implement as much logic as You can.

We can now extend the Use Case interfaces and add the missing logic:

  • Extend ManagePersonUc by adding the following method:

PersonCto savePerson(PersonCto personCto);
  • Create ManageTaskListUc interface with the following methods:

TaskListCto saveTaskList(TaskListCto taskListCto);
void deleteTaskList(Long id);
  • Create FindTaskListUc interface with the following methods:

List<TaskListEto> findAllTaskLists();
Optional<TaskListCto> findTaskList(Long id);
  • In the corresponding Use Case interface add a method for creating TaskList with the given name and given number of TaskItems (TaskItems should be created with some arbitrary data)

  • In the corresponding Use Case interface add a method for finding TaskList by name

  • In the corresponding Use Case interface add a method for finding overdue and uncompleted TaskItems

2.3. Use Case implementation

Please implement all the unimplemented methods added in the previous step.

2.4. Tests

Please cover all the newly implemented methods from the previous step with the JUnit tests.

3. (Optional) Bean validation using Hibernate Validator

Note
This is an optional exercise, if You implemented the previous tasks, feel free to try it out.

In this exercise we will implement the validation of the Transfer Objects using Hibernate Validator.

3.1. Configuration

Starting with Boot 2.3, we need to explicitly add the spring-boot-starter-validation dependency to pom.xml. It was also possible to add it via Spring Initializr. Please add the following dependency if it is missing:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3.2. Constraint definition

Hibernate Validator offers validation annotations that can be applied to the data fields within our classes. For example if we would like to ensure that the PersonEto will contain a non-empty, valid email address we can annotate it as follows:

@Data
@Builder
public class PersonEto  {
    private final Long id;
    private final int version;
    @NotEmpty
    @Email
    private final String email;
}

or if You created a record instead:

@Builder
public record PersonEto (Long id,
                         int version,
                         @NotEmpty @Email String email) { }

You can similarly annotate other fields in ETOs. For example, please make sure that the name of the TaskList contains at least 5 characters and the name of the TaskItem contains from 2 to 40 characters. Please check this for further reference or help: https://hibernate.org/validator/.

3.3. Constraint validation

The validation will not work out-of-the box. To enable it we have to put the @Valid annotation on the method parameters or fields to tell Spring that we want a method parameter or field to be validated. We should annotate at least the method parameter in the interface, but it is considered a good practice to annotate it also in the implementation. Additionally, we should add a class-level @Validated annotation to tell Spring to validate parameters that are passed into a method of the annotated class.

If we want to do it for the ManagePersonUc Use Case, then the interface and implementation should look as follows:

public interface ManagePersonUc {

    PersonEto savePerson(@Valid PersonEto personEto);

    // ...
}
@Service
@Transactional
@Validated
public class ManagePersonUcImpl implements ManagePersonUc {

   // ...

    @Override
    public PersonEto savePerson(@Valid PersonEto personEto) {

        // ...
    }

    // ...
}

Please add similar validations for other Use Cases.

3.4. Tests

Please add some test to verify that the added validations work as expected.

4. (Optional) Bean mapping using MapStruct

Note
This is an optional exercise, if You implemented the previous tasks, feel free to try it out.

In this exercise we will implement the automatic mapping between Entities and Transfer Objects using MapStruct framework.

4.1. Configuration

To use MapStruct we need to add the dependency to the pom.xml. At the time of writing the most recent MapStruct version is 1.5.5.Final. The current version can be checked here: https://mapstruct.org/documentation/installation/.

Please add the following dependencies (I recommend defining the version as a Maven property):

    <properties>
        <java.version>21</java.version>
        <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
    </properties>

    ...

    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>${org.mapstruct.version}</version>
        <scope>provided</scope>
    </dependency>

4.2. Mappers

MapStruct is a code generator that simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. To generate a mapper we will create a mapping interface annotated with @Mapper. By default, MapStruct will automatically map properties where the property name and types match. It will also map automatically if it can safely do an implicit type conversation.

Here is the example of the Mapper for mapping between PersonEntity and PersonEto:

@Mapper(componentModel = "spring")
public interface PersonMapper {

    PersonEto toPersonEto(PersonEntity personEntity);

    PersonEntity toPersonEntity(PersonEto personEto);
}

Please add the mappers for each Entity/Eto and put them into the following package:

com.capgemini.training.todo.task.logic.mapper

MapStruct will generate the implementation for us! Of course, we can customize the mappings, but in our case this will not be necessary. If You are interested, please check the example and the documentation here: https://mapstruct.org/.

4.3. Bean mapping

The mapper can be now injected into our Use Case implementations as any other Spring Component:

@Service
@Transactional
public class ManagePersonUcImpl implements ManagePersonUc {

    private final PersonRepository personRepository;
    private final PersonMapper personMapper;

    public ManagePersonUcImpl(PersonRepository personRepository, PersonMapper personMapper) {
        this.personRepository = personRepository;
        this.personMapper = personMapper;
    }

    @Override
    public PersonEto savePerson(PersonEto personEto) {

        PersonEntity personEntity = personMapper.toPersonEntity(personEto);
        personEntity = personRepository.save(personEntity);
        return personMapper.toPersonEto(personEntity);
    }

    // ...
}

Please inject the mappers and use them for the Entity/Eto mappings. Then, remove all the methods needed for manual mapping from all the Use Case implementation.

4.4. Tests

You can add some tests for the mappers. However, the mapping should be already covered by the existing tests, might be that some tests will need to be adapted, but it is perfectly fine to just re-run the existing tests and check if the application still works as expected.

Note
If you want to incorporate mappers to be used in existing unit tests, you can consider using of @Spy like shown below. Remember, don’t use @Autowired in tests annotated with @ExtendWith(MockitoExtension.class), because in unit tests there is no spring context started and @Autowired will not work.
    @Spy
    private TaskItemMapper taskItemMapper = new TaskItemMapperImpl();