post-thumb

Software Patterns - From Textbook To Reality

Introduction

You studied design patterns and accepted their value - but somehow it is difficult to remember when you last used one? Well good news, chances are you do use them! Design Patterns left the textbooks behind and escaped to reality!

The original software design patterns are pure, object-oriented and imperative. Modern programming languages offer meta programming through annotations and real-world projects use frameworks and libraries.

These software patterns might look different in the class diagram and some can be reduced to a single line of code or one annotation. However, the rationals and tradeoffs for applying them remain valid. As does the level of documentation and understanding that can be shared between developers by naming them. There is a lot of information flowing if your colleague tells you that a piece of code is a Proxy or Adapter or Abstract Factory.

Let us set sails and rediscover some common patterns.

The Singleton Pattern

Because which article about patterns wouldn’t start with the Singleton.

@Bean
class X {}

// or slightly longer
@Scope("singleton")
@Bean
class X {}

With the Spring framework that is it. Bean Scopes default to Singleton and the framework cares about all the nasty thread safe initialization procedures.

Now that you know (or remember), there are a few interesting things to observe:

  • the default is Singleton => from the framework designers perspective this seems to be quite a powerful pattern
  • there are a lot of implications for multi-threading, shared usage, resource access, memory footprint …
  • all of that knowledge is instantly transferred if your peers understand and recognize the pattern

The Observer Pattern

« Don’t call me, I call you. » The Observer Pattern is a very powerful yet simple pattern to decouple objects and runtime relationships. Its usage scenarios include user interfaces, event handling and generally notification systems. The naming varies, from Java’s ubiquitous Listeners to Publisher and Subscriber or the classic Observer, but the intention remains the same.

Message Bus Systems that provide topics follow a similar purpose: They publish an event, and whoever needs it reacts to it. But, compared to the Observer Pattern with a stronger focus on decoupling and asynchronous communication. And then there are hybrids like Spring Events which by default focus on decoupling and synchronous communication.

@Component
public class SpringEventListener {
    
    @EventListener
    public void doSomething(MyEvent event) {
        // did something
    }
}

@Service
@RequiredArgsConstructor
public class SpringEventPublisher {
    private final ApplicationEventPublisher event;

    private void publishSomething() {
        events.publishEvent(new MyEvent());
    }
}

The textbook Observer can be spotted as integral part of standard libraries & languages but in a wider sense the pattern is also present in infrastructure like JMS or Kafka and framework notification systems like Spring Events:

  • the Observer is always about propagating change to interested parties
  • on an architectural level, this extends to decoupling; Spring Modulith provides a concise example
  • finally remember the pitfall, Observers & Message Topics typically lack order guarantees

The Chain of Responsibility Pattern

This pattern is used by frameworks to make applications more flexible and maintainable. To understand the power of this pattern let us study two examples.

First take a look at Spring Boot request flow, there are a lot of different filters for handling the security, the logging, the request handling and so on. Let us create a simple additional filter to write some additional logs when the request is handled.

public class CustomFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // Pre-process request
        System.out.println("Request pre-processing in CustomFilter");

        // Pass the request/response to the next filter in the chain
        chain.doFilter(request, response);

        // Post-process response
        System.out.println("Response post-processing in CustomFilter");
    }

    @Override
    public void destroy() {}
}

All request filters are called one after the other they create a chain of responsibilities. The ordering of the chain can be influenced by the @Order annotation or when you need more control by a FilterRegistrationBean. As we can see in the example above, each filter is calling the next filter in the chain by calling the chain.doFilter(request, response) method.

The same principle is used in the Symfony framework . Symfony is dispatching the kernel.request event. There are several listeners like LocaleListener, the RouterListener or the SessionListener registered to this event. Let us again create a custom filter to add some logging.

namespace App\EventListener;

use Symfony\Component\HttpKernel\Event\RequestEvent;

class MyRequestListener
{
    public function onKernelRequest(RequestEvent $event)
    {
        $request = $event->getRequest();
        
        $method = $request->getMethod();
        $uri = $request->getRequestUri();

        // Example to log the request method and uri
        error_log("Request: $method $uri");
    }
}

Next register the listener in the services.yaml (or any other configuration file).

services:
    App\EventListener\MyRequestListener:
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 0 }

To influence the chain order, we can use the priority attribute (-255 and 255).

Both examples are implementations of the “Chain of Responsibility” pattern but in contrast to the textbooks the names are slightly different Filter or Listener instead of Handlers. We also didn’t wire them explicitly. Instead, we rely on the framework to add our code to pre-existing processing Chains and influence the order - where necessary.

  • try to spot more “Chain of Responsibility” implementations (think about logging, de/encoding, transformations)
  • spot the order mechanisms, they are key to properly use the chain and utilize the pattern
  • remember the Open/Closed principle, test Handlers in isolation and move them between use cases

The Visitor Pattern

As one of the original GoF patterns the visitor pattern has been out there for quite some time. Remember: the pattern allows us to add new functionality to existing object structures without modifications of the underlying classes. It promotes flexibility and follows the Open/Closed principle.

A common example is an extensible export functionality for a tree of objects, where each concrete visitor would implement one target format.

Most modern programming languages allow us to achieve the same thing with pattern matching. Good old Java does it like this

sealed interface Element permits A, B { }

for (Element e : elements) {
   visitor.visit(element);
}

class ConcreteVisitor {
    void visit(Element e) {
        switch (e) {
            case A a -> System.out.println("visiting an A");
            case B b -> System.out.println("visiting a B");
        }
    }
}

and very similar in RUST

enum Element {A, B}

for e in &elements {
        visitor.visit(e);
    }
}

impl ConcreteVisitor {
    fn visit(&self, e: &Element) {
        match e {
            Element::A => println!("visiting an A"),
            Element::B => println!("visiting a B"),
        }
    }
}

As Nicolai Parlog points out, sealing the Java trait (or respectively using an enum in RUST) and avoiding a default clause in the pattern matching is key. This gets us compile time safety checks for new types of elements that might not be handled in the code base.

  • use pattern matching to implement visitors with a bare minimum of code
  • do not add default clauses and use sealed / enumerable sets of objects
  • make it obvious if a pattern matcher is a visitor to give your colleagues the right clues

The Adapter Pattern

The adapter pattern is used to make two incompatible interfaces compatible. You often find the adapter pattern when you want to integrate legacy code, third party libraries or systems.

An example from PHP and the Symfony framework is the Notifier component. The Notifier component is an adapter to send notifications. The component is hiding the complexity of the different transports like email, slack, sms, and provides a simple interface to send notifications.

This shows how to send a Slack message with the Notifier component when we use the component standalone.

$transport = new SlackTransport("My-Slack-Access-Token", "My-Slack-Channel");
$chatter = new Chatter($transport);
$chatter->send(new ChatMessage("Hello World!"));

We do not need to know how the Slack API works, we just use the simple interface from the Notifier component. When we want to send the same message to Microsoft Teams, we just need to change the transport.

$transport = new MicrosoftTeamsTransport("webhookb2/...");
$chatter = new Chatter($transport);
$chatter->send(new ChatMessage("Hello World!"));

If you look at modular frameworks like Spring, adapters are everywhere

  • learn to identify them and recognize extension points that can hold your own integrations
  • the adapter makes it easy to exchange a third party library or system without changing the business logic
  • keep the business logic clean, easy to test and maintain

The State Pattern

The State pattern allows objects to change their behavior dynamically based on their internal state. Famously it avoids screen long if - else branching by separating code following the Open/Closed and Single Responsibility Principles.

Let us look how frameworks implement the state pattern. This example of the Symfony workflow component shows how it can be used.

framework:
    workflows:
        article_publishing:
            type: 'workflow'
            supports: ['App\Entity\Article']
            places:
                - draft
                - review
                - published
            transitions:
                to_review:
                    from: draft
                    to: review
                publish:
                    from: review
                    to: published

There are three states in the workflow: draft, review and published, with the transitions to_review and publish defining how the object can change.

To implement the same workflow with Spring Boot, we can use the spring state machine.

@Configuration
@EnableStateMachine
public class StateMachineConfig extends StateMachineConfigurerAdapter<String, String> {

    @Override
    public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
        states
                .withStates()
                .initial("DRAFT")
                .state("REVIEW")
                .end("PUBLISHED");
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
        transitions
                .withExternal().source("DRAFT").target("REVIEW").event("TO_REVIEW")
                .and()
                .withExternal().source("REVIEW").target("PUBLISHED").event("PUBLISH");
    }
}

With these configurations we can easily implement a state machine and see which changes of the state are allowed.

  • large frameworks come with their own implementations of the state pattern; alternatively, using a fitting library this saves you lots of headaches during implementation
  • using the pattern results in cleaner and more maintainable code by separating state handling from the business logic
  • utilizing what is already there reduces the risk of overspilling efforts (architects love their states) and clearly communicates intention and capabilities of a concrete State Machine implementation (plus there might be fewer bugs)

Conclusion

In real enterprise source code most patterns are not codified like their textbook examples. The naming differs, frameworks and libraries provide ready to use implementations and language features and meta programming allow to shrink patterns, some even to the extend of declarative programming.

But the use cases and principles behind the patterns stand the test of time. If you are able to spot and name patterns, you can apply and communicate all your textbook knowledge about their properties, pitfalls and intent.

Software Patterns escaped the textbooks and are part of business reality!