Spring patterns. Fluent interface / Hebrew

Spring patterns. Fluent interface / Hebrew

Hello everyone.

I develop applications using Java, Spring Boot, Hibernate.

In this article, I want to share the experience of creating a Fluent Interface, but not the classic GOF template, but using Spring.

A classic example of Java’s Fluent Interface pattern is the Stream API. I’ll show you how to write something similar using Spring.

Example client code:

 Participant participant = testEntityFactory.participantBy(RankType.WRITER)
            .withName("customName")
            .withPost("post_1")
            .withPost("post_2")
            .withPost("post_3")
            .createOne();

Preface

We all know how easy it is to implement the Chain of Responsibility pattern using Spring:

@Autowired
private List implementations;

Run various entities through chains, it is convenient to expand the chain with the help of new implementations. And if I want to use not the whole chain, but only a part, and dynamically change it in the client code? I will show you now.

I will show on the example of a module that will be responsible for creating test entities.

1. Preparation of the graph of entities

For example, you need to prepare the graph @Entity. We will need 3 @Entities: Participant, Rank, Post. There will be no example code, the usual @Entity. Participant has one Rank and many Posts. Rank can be of 3 types:

public enum RankType {

    READER, WRITER, MODERATOR

}

Values ​​are usually written in DB scripts. For example, it does not matter, so the ddl scripts will be generated by Hibernate, and the value for Rank – RankInitializer, when raising the Spring context:

  @PostConstruct
    void init() {
        List ranks = Arrays.stream(RankType.values())
            .map(rankType -> new Rank().setRankType(rankType))
            .toList();
        rankRepository.saveAll(ranks);
    }

2. Classic creation of test entities

I will use SpringBootTest and TestContainers to write the tests. Let’s create the SpringBootTestApplication class, from which the Spring Boot tests will be inherited.

Let’s imagine that we need to write a test for which we need to prepare an entity. The classic solution looks like this:

class ManualImperativeCreatingExampleTest extends SpringBootTestApplication {

    @Test
    void test() {
        Participant participant = createParticipantManualAndImperative("name", "post", RankType.WRITER);

        Assertions.assertEquals(RankType.WRITER, participant.getRank().getRankType());
    }

    private Participant createParticipantManualAndImperative(String participantName, String postTitle, RankType rankType) {
        Rank rank = rankRepository.findByRankType(rankType); // вытащить Rank из бд

        Participant participant = new Participant() // создать новый Participant
            .setRank(rank)
            .setName(participantName);
        participantRepository.save(participant); // заперсистить

        Post post = new Post() // создать новый Post
            .setTitle(postTitle);
        postRepository.save(post); // заперсистить

        // переженить Participant и Post
        participant.getPosts().add(post);
        participantRepository.save(participant);

        post.setParticipant(participant);
        postRepository.save(post);

        return participant;
    }

}

A private method will most likely be responsible for this initially. Then the method will be raised higher – to the class from which all Spring Boot tests are inherited. They will make a protected method. Then a million more methods with other signatures will appear nearby. Then it turns out that it would be nice to wrap the entity creation in a transaction. Well this can easily be done using TransactionTemplate. As a result – some methods with a transaction, some without. A class like SpringBootTestApplication turns into an anti-pattern static junk – a multi-line class that has everything, while it takes a lot of time to find something without being the author, and methods with a similar signature or method name try to explain how they differ from each other, or the java-doc written above, which often does not correspond to the current implementation of the method… It’s enough to put up with it 😉

3. Introduction to TestEntityFactory

I prefer to spread the classes out of their responsibility. Therefore, the example will have separate classes for the facade (TestEntityFactory), the callback collector (ParticipantPrototype), the callback (Callback), the bean with the implementation of non-terminal operations (ParticipantPrototypeService), the bean with the implementation of terminal operations (ParticipantPrototypeFinisher).

Read more about each of these classes below.

  1. TestEntityFactory – the facade that provides access to the module is a Spring Singleton.

@Service
@RequiredArgsConstructor
public class TestEntityFactory {

    private final ParticipantPrototypeService participantPrototypeService;

    public ParticipantPrototype participantBy(RankType rankType) {
        return new ParticipantPrototype()
            .setRankType(rankType)
            .setParticipantPrototypeService(participantPrototypeService);
    }

}

In the client code, access to the API of the module will be through TestEntityFactory. Example:

@Autowired
protected TestEntityFactory testEntityFactory;
  1. Callback – Functional interface, in the implementations of which it is necessary to tell how we want to modify the entity.

@FunctionalInterface
public interface Callback {

    void modify(Participant participant);

}

You can use Consumer, but the apply() method is not as expressive as modify() in this context. You can also use UnaryOperator (this is Function) and build chains through andThen(). But I prefer to build a chain with List.

  1. ParticipantPrototype – POJO that will collect List and provide access to the module’s terminal and non-terminal methods.

@Getter
@Setter
@Accessors(chain = true)
public class ParticipantPrototype {

    private RankType rankType;
    private ParticipantPrototypeService participantPrototypeService;
    private List chain = new ArrayList();
    private int amount;

    /**
     * terminal operations
     */
    public Participant createOne() {
        this.amount = 1;
        return participantPrototypeService.create(this).get(0);
    }

    public List createMany(int amount) {
        this.amount = amount;
        return participantPrototypeService.create(this);
    }

    /**
     * intermediate operations
     */
    public ParticipantPrototype with(Callback callback) {
        chain.add(callback);
        return this;
    }

    public ParticipantPrototype withName(String participantName) {
        chain.add(participant -> participant.setName(participantName));
        return this;
    }

    public ParticipantPrototype withPost(String customTitle) {
        chain.add(participant -> participantPrototypeService.addPost(participant, customTitle));
        return this;
    }

}

You can use the spring scope prototype, but only 1 service is needed here, and it is easier to pass it when creating the class. Thereby, responsibility was divided from information gathering to entity creation and implementation of terminal and terminal operations methods.

  1. ParticipantPrototypeFinisher – Spring Singleton, which is responsible for terminal operations.

@Service
@RequiredArgsConstructor
public class ParticipantPrototypeFinisher {

    private final ParticipantRepository participantRepository;
    private final RankRepository rankRepository;

    @Transactional
    public List create(ParticipantPrototype prototype) {
        List result = new ArrayList();
        for (int i = 0; i  chain = prototype.getChain();
        chain.forEach(callback -> callback.modify(participant));

        return participant;
    }

}

5. ParticipantPrototypeService – Spring Singleton contains methods with implementation of non-terminal operations. Added an example of creating a Post. Also, through this class, terminal operations are delegated to ParticipantprototypeFinisher.

@Service
@RequiredArgsConstructor
public class ParticipantPrototypeService {

    private final ParticipantPrototypeFinisher participantPrototypeFinisher;
    private final ParticipantRepository participantRepository;
    private final PostRepository postRepository;

    public List create(ParticipantPrototype prototype) {
        return participantPrototypeFinisher.create(prototype);
    }

    public void addPost(Participant participant, String postTitle) {
        Post post = new Post()
            .setTitle(postTitle)
            .setParticipant(participant);
        postRepository.save(post);

        participant.getPosts().add(post);
        participantRepository.save(participant); // добавил для наглядности, мы в транзакции, сохранит при commit и без .save()
    }

}

4. How does the module work?

When we wrote something like this in the client code:

 Participant participant = testEntityFactory.participantBy(RankType.WRITER)
            .withName("customName")
            .withPost("post_1")
            .withPost("post_2")
            .withPost("post_3")
            .createOne();

The following is happening under the hood:

  1. In the first line, we create a prototype. The creation signature must contain a set of parameters that are necessary to create a “minimum permissible entity”. That is, such an entity that can be “persisted” without getting an exception that the field X should not be null or any other validations at the DB/ORM level. In my example, participant should have name and rank. I decided that I should put rank, and name – you can do with the default value.

  2. Lines 2 through 5 tell us how we want to modify the “minimum admissible entity”. By default, there is a with() method where you can pass the lambda:

   public ParticipantPrototype with(Callback callback) {
        chain.add(callback);
        return this;
    }

If you have repeating lambdas, encapsulate them in a method.

If the modification is simple, implement it in a method:

  public ParticipantPrototype withName(String participantName) {
        chain.add(participant -> participant.setName(participantName));
        return this;
    }

If the method needs other spring beans, for example Repository – pass the work to ParticipantPrototypeService – it is a spring bean and can be injected via @Autowired.

 public ParticipantPrototype withPost(String customTitle) {
        chain.add(participant -> participantPrototypeService.addPost(participant, customTitle));
        return this;
    }

Callback will be added to the chain (List), and will be called in a terminal operation.

  1. The createOne method is a terminal operation. FluentInterface is lazy, meaning that non-terminal operations will only start processing when a terminal operation starts. This way we gain additional control over entity creation and can surround the entire transaction creation.

@Transactional
public List create(ParticipantPrototype prototype) {

Furthermore, if we need to create multiple entities, we can ask the factory to do so using the createMany(int amount) method.

    /**
     * terminal operations
     */
    public Participant createOne() {
        this.amount = 1;
        return participantPrototypeService.create(this).get(0);
    }

    public List createMany(int amount) {
        this.amount = amount;
        return participantPrototypeService.create(this);
    }

Next, the ParticipantPrototypeFinisher, in a transaction, creates a “minimum admissible entity”, “persists” it, fills it with default values, modifies it using a chain of non-terminal operations specified in the client code, and sends it to the top.

Completion

On the example of the implementation of the module for the creation of test entities, we considered the creation of the spring fluent interface pattern.

This approach allows writing good, declarative, easily expandable code that also allows:

  1. In already written tests, do not be distracted by the noise of creating test entities and see the structure of only those fields that are important for the test, and the rest of the fields that are not important for the test will have default values.

  2. In new tests, reuse the module and extend it by adding new methods.

  3. In case of changing some fields in the essence, make corrections in one place – in the “guts” of the module. Instead of editing signatures throughout the project, which would have to be done if the classic static scrap approach is used.

  4. Encapsulate the code responsible for creating test entities from a class common to all SpringBootTest classes into a separate module.

  5. If laziness is not needed – you can remove the list of lambdas and do the work immediately, then the spring builder pattern will turn out.

You can see the code here

Examples in the FluentInterfaceExampleTest class.

Related posts