Methodcentipede

Once upon a time, as a child, I lay on my bed and looked at the patterns on the old Soviet carpet for a long time, seeing animals and fantastic figures in them. Now I look at the code more often, but similar images are still born in my mind. Just like on the carpet, these images form repeating patterns. They can be both pleasant and repulsive. Today I want to tell you about such an unpleasant pattern that occurs in programming.

Scenario

Imagine a service that processes a client registration request and sends an event about it to the system. In this article, I will show an example of implementation that I consider an anti-pattern and propose a corrected version.

Option 1: Methodcentipede

The Java code below shows the code for the RegistrationService class that handles the request and dispatches the event.

public class RegistrationService {

    private final ClientRepository clientRepository;
    private final KafkaTemplate<Object, Object> kafkaTemplate;
    private final ObjectMapper objectMapper;

    public void registerClient(RegistrationRequest request) {
        var client = clientRepository.save(Client.builder()
                .email(request.email())
                .firstName(request.firstName())
                .lastName(request.lastName())
                .build());
        sendEvent(client);
    }

    @SneakyThrows
    private void sendEvent(Client client) {
        var event = RegistrationEvent.builder()
                .clientId(client.getId())
                .email(client.getEmail())
                .firstName(client.getFirstName())
                .lastName(client.getLastName())
                .build();
        Message message = MessageBuilder
                .withPayload(objectMapper.writeValueAsString(event))
                .setHeader(KafkaHeaders.TOPIC, "topic-registration")
                .setHeader(KafkaHeaders.KEY, client.getEmail())
                .build();
        kafkaTemplate.send(message).get();
    }

    @Builder
    public record RegistrationEvent(int clientId, String email, String firstName, String lastName) {}
}

The structure of the code can be simplified as follows:

Here you can see that the methods form an unbroken chain through which the data flows, like a long, narrow intestine. The methods in the middle of this chain are responsible not only for the logic directly described in their body, but also for the logic of the methods they call and their contracts (for example, the need to handle certain errors). All methods preceding the called one inherit all its complexity. For example, if kafkaTemplate.send has a side effect in the form of sending an event, and the one calling it sendEvent acquires the same side effect. Method sendEvent is also responsible for serialization, including handling its errors. Testing individual parts of the code is complicated by the fact that there is no way to test each part in isolation without using mocks.

Option 2: Corrected version

Code:

public class RegistrationService {

    private final ClientRepository clientRepository;
    private final KafkaTemplate<Object, Object> kafkaTemplate;
    private final ObjectMapper objectMapper;

    @SneakyThrows
    public void registerClient(RegistrationRequest request) {
        var client = clientRepository.save(Client.builder()
                .email(request.email())
                .firstName(request.firstName())
                .lastName(request.lastName())
                .build());
        Message<String> message = mapToEventMessage(client);
        kafkaTemplate.send(message).get();
    }

    private Message<String> mapToEventMessage(Client client) throws JsonProcessingException {
        var event = RegistrationEvent.builder()
                .clientId(client.getId())
                .email(client.getEmail())
                .firstName(client.getFirstName())
                .lastName(client.getLastName())
                .build();
        return MessageBuilder
                .withPayload(objectMapper.writeValueAsString(event))
                .setHeader(KafkaHeaders.TOPIC, "topic-registration")
                .setHeader(KafkaHeaders.KEY, event.email)
                .build();
    }

    @Builder
    public record RegistrationEvent(int clientId, String email, String firstName, String lastName) {}
}

The diagram is presented below:

Here you can see that the method sendEvent not at all, and is responsible for sending kafkaTemplate.send. The entire process of constructing a message for Kafka is moved to a separate method mapToEventMessage. Method mapToEventMessage has no side effects, its responsibility boundary is clearly defined. Exceptions related to serialization and message sending are part of the contract of individual methods and can be individually handled.

Method mapToEventMessage is a pure function. When a function is deterministic and has no side effects, we call it a “pure” function. Pure functions:

  • easier to read,

  • easier to debug,

  • easier to test,

  • do not depend on the order in which they are called,

  • just run in parallel.

Recommendations

I would suggest the following techniques to help avoid such anti-patterns in your code:

All these techniques are closely related and complement each other.

Testing Trophy

This is an approach to code coverage with tests, which focuses on integration tests that check the service contract as a whole. Unit tests are used for individual functions that are difficult or expensive to test through integration tests. I described tests with a similar approach in my articles: We distribute stages of testing http requests in Spring on shelves, Increasing the visibility of integration tests, Isolation in tests with Kafka.

One Pile

This technique is described in the book “Tidy First?” by Kent Beck. The main idea: reading and understanding code is harder than writing it. If the code is broken into too many small parts, it can be useful to first combine it into a whole to see the overall structure and logic, and then break it down again into more understandable pieces.

In the context of this article, it is suggested not to split code into methods unless it ensures the required contract is fulfilled.

Test Driven Development

This approach allows us to divide the efforts between writing code to implement the contract and forming the code design. We do not try to make a good design and write code that meets the requirements at once, but we separate these tasks. The development process looks like this:

  1. We write tests for the service contract using the Testing Trophy approach.

  2. We write code in the One Pile style, ensuring that it ensures the execution of the required contract. We do not pay attention to the quality of the code design.

  3. We refactor the code. All the code is written, we have a full understanding of the implementation and possible bottlenecks.

Conclusion

This article discusses an example of an anti-pattern that can lead to difficulties in maintaining and testing code. Approaches such as Testing Trophy, One Pile, and Test-Driven Development allow us to structure work in such a way that the code does not turn into an impassable labyrinth. By investing time in proper code organization, we lay the foundation for long-term sustainability and ease of maintenance of our software products.

Thanks for reading this article, and good luck in your quest to write simple code!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *