Neste post vamos aprender 3 dos design patterns essenciais para o desenvolvimento de software utilizando a linguagem ´Java´ e novas features adicionadas na versão 21 e 24 do java.
Este projeto simula um sistema de cadastro de usuários, utilizando os padrões de design Singleton, Factory e Observer. Cada padrão foi escolhido para resolver problemas específicos dentro da arquitetura.
Primeiro criamos a classe UsuarioController que tera a responsabilidade de expor nossa api para ser acessada via rest através do meto http POST.
UsuarioController

package com.designpatterns.application.controller;

import com.designpatterns.application.mapper.input.UsuarioRequest;
import com.designpatterns.application.mapper.output.UsuarioResponse;
import com.designpatterns.core.usuario.UsuarioService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UsuarioController {
    private final UsuarioService usuarioService;

    public UsuarioController(UsuarioService usuarioService) {
        this.usuarioService = usuarioService;
    }

    @PostMapping("usuario")
    public UsuarioResponse cadastraUsuario(@RequestBody UsuarioRequest request) {
        return usuarioService.createUser(request.converteUsuario()).converteResponse();
    }
}

A classe UsuarioController depende de um objeto do tipo UsuarioService para realizar suas operações. Em vez de criar diretamente uma instância de UsuarioService (o que resultaria em alto acoplamento), definimos essa dependência como um parâmetro no construtor utilizando a injeção de dependência do spring, quando o Spring Container detecta essa classe, ele identifica o construtor e injeta automaticamente uma instância de UsuarioService, que já foi configurada no contexto da aplicação (geralmente definida como um Bean no Spring).
Observação: Nesse momento já estamos utilizando o design pattern Singleton pois o Spring fornece essa instância única em toda a aplicação, que já foi criada e gerenciada pelo Container Spring, caso fosse necessário que tivesses uma classe nova todas as vezes que instanciada em um constructor e necessário utilizarmos a annotation @Scope("prototype") para referenciar esse contexto ao Spring conforme a imagem abaixo.

Image description

Logo mais veremos outra forma de utilizar o design pattern Singleton.
UsuarioService

package com.designpatterns.core.usuario;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

@Service
public class UsuarioService {
    private final ApplicationEventPublisher eventPublisher;

    public UsuarioService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }


    public Usuario createUser(Usuario usuario) {
        System.out.println("Usuário cadastrado com sucesso: " + usuario.nome());
        eventPublisher.publishEvent(new UsuarioCadastradoEvent(this, usuario));
        return usuario;
    }
}

Dentro da nossa classe de serviço UsuarioService encontramos outro design pattern que podemos utilizar, o Observer.
O Observer é utilizado para desacoplar o envio de notificações do fluxo de cadastro de usuário. Quando um usuário é cadastrado, múltiplos observadores podem reagir a esse evento de maneira independente.
Temos injetado em nossa classe de serviço a classe ApplicationEventPublisher que é uma interface do Spring Framework responsável por publicar eventos dentro do contexto da aplicação, nos permitindo que o UsuarioService publique eventos de forma desacoplada. Ou seja, o service não precisa conhecer diretamente os objetos que vão reagir ao evento.
OBS: A implementação do padrão Observer utilizando o ApplicationEventPublishere uma abordagem moderna e desacoplada provida pelo Spring. Com isso, sempre que um novo usuário é cadastrado, um evento do tipo UsuarioCadastradoEvent é publicado.

Esse evento sera tratado pela classe UsuarioEventListener, que atua como "observador", reagindo ao evento publicado para enviar notificações com base na preferência do usuário (SMS ou E-mail).

Essa abordagem traz os benefícios do padrão Observer (desacoplamento e reatividade), utilizando os recursos do próprio Spring.

Nesse caso, ela dispara o evento UsuarioCadastradoEvent com o usuário recém-criado. O Spring então notifica automaticamente qualquer classe anotada com @EventListener e que esteja ouvindo esse tipo de evento.
Vamos criar entao a classe UsuarioCadastradoEvent e também o listener responsável por ouvir esse evento e implementar o envio de notificação de email/sms.
UsuarioCadastradoEvent

package com.designpatterns.core.usuario;

import org.springframework.context.ApplicationEvent;

public class UsuarioCadastradoEvent extends ApplicationEvent {

    private final Usuario usuario;
    public UsuarioCadastradoEvent(Object source, Usuario usuario) {
        super(source);
        this.usuario = usuario;
    }

    public Usuario getUsuario() { return usuario; }
}

UsuarioEventListener

package com.designpatterns.core.usuario;

import com.designpatterns.core.notification.EmailNotification;
import com.designpatterns.core.notification.NotificationFactory;
import com.designpatterns.core.notification.SmsNotification;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class UsuarioEventListener {
    private final NotificationFactory factory;

    public UsuarioEventListener(NotificationFactory factory) {
        this.factory = factory;
    }

    @EventListener
    public void handle(UsuarioCadastradoEvent event) {
        Usuario usuario = event.getUsuario();
        factory.getNotificationsFor(usuario.preferencia())
               .forEach(n -> {
                   if (n instanceof EmailNotification email) {
                       email.notificate(usuario, "E-mail: Bem-vindo, " + usuario.nome() + "!");
                   } else if (n instanceof SmsNotification sms) {
                       sms.notificate(usuario, "SMS: Bem-vindo, " + usuario.nome() + "!");
                   }
               });
    }
}

Observação: Note que utilizamos o instanceof com pattern matching, o uso de pattern matching com instanceof facilita muito o código ao evitar castings explícitos. Isso torna o código mais legível, seguro e moderno, nos permitindo acessar diretamente a instância com cast implícito e mais legibilidade sendo muito útil quando queremos tratar notificações de forma personalizada por tipo uma das melhorias de linguagem do Java 24

Queremos em nosso sistema notificar o usuário via email e sms que o cadastro foi efetuado com sucesso.
Vamos utilizar um novo design pattern dentro da classe que esta ouvindo o email através da classe NotificationFactory
No projeto, a classe NotificationFactory contem instâncias de diferentes tipos de notificadores, como EmailNotification e SmsNotification, sem que o código cliente precise conhecer os detalhes específicos de cada implementação. Isso é alcançado através do design pattern Factory, que promove a criação de objetos de forma encapsulada e flexível.

🧱 Factory Pattern com switch expression e Injeção de Dependência
Aqui, vamos implementar uma Factory que retorna uma lista de notificações com base na preferência do usuário, como Email, SMS, ou ambos. Vamos usar o padrão Factory para encapsular a lógica de criação dessas instâncias.
NotificationFactory

package com.designpatterns.core.notification;

import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class NotificationFactory {

    private final EmailNotification email;
    private final SmsNotification sms;

    public NotificationFactory(EmailNotification email, SmsNotification sms) {
        this.email = email;
        this.sms = sms;
    }
    public List getNotificationsFor(String preferencia) {
        return switch (preferencia.toLowerCase()) {
            case "email" -> List.of(email);
            case "sms" -> List.of(sms);
            case "ambos" -> List.of(email, sms);
            default -> List.of();
        };
    }
}

🏭 O que essa Factory faz?
A NotificationFactory decide, com base na preferência do usuário (se ele deseja receber notificações por email, SMS ou ambos), qual(is) instância(s) de notificação retornar. A implementação do switch expression torna o código mais limpo, sem a necessidade de múltiplos if ou else.

💡 O que foi aplicado de moderno aqui?
Injeção de Dependência com Spring (@Component): As instâncias de EmailNotification e SmsNotification são injetadas automaticamente pelo Spring, facilitando o gerenciamento das dependências.

switch expression (Java 14+): Melhorias no Java que tornam a seleção de valores mais legível e com menos código boilerplate, comparado ao switch tradicional.

✅ Exemplo de uso da Factory:

NotificationFactory factory = new NotificationFactory(email, sms);
List notifications = factory.getNotificationsFor("ambos");

notifications.forEach(notification -> notification.send());

🔄 O que mais podemos fazer?
Flexibilidade: Caso surjam novos tipos de notificação, como Push ou WhatsApp, podemos facilmente estender a Factory para lidar com esses novos tipos.

Extensibilidade: Adicionar novas opções ao método getNotificationsFor se torna trivial, mantendo o código limpo e expansível.

Observação: Criaremos a interface Notification e as classes EmailNotification e 'SmsNotification' que implementam essa interface.

No projeto, usamos a seguinte construção moderna das versões recentes do java para criar a interface Notification

Image description
Essa linha define uma interface selada (sealed interface), que traz uma poderosa funcionalidade: restringir quais classes podem implementar ou estender a interface.

Notification

package com.designpatterns.core.notification;

import com.designpatterns.core.usuario.Usuario;

public sealed interface Notification permits EmailNotification, SmsNotification {
    void notificate(Usuario usuario, String mensagem);
}

Agora vamos criar as classes EmailNotification e SmsNotification que implementam a interface Notification e através do método notificate enviam a mensagem ao usuário.
EmailNotification

package com.designpatterns.core.notification;

import com.designpatterns.core.usuario.Usuario;
import org.springframework.stereotype.Component;

@Component
public final class EmailNotification implements Notification {
    public void notificate(Usuario usuario, String mensagem) {
        System.out.printf("Enviando EMAIL para %s (%s): %s%n",
                usuario.nome(), usuario.email(), mensagem);
    }
}

SmsNotification

package com.designpatterns.core.notification;

import com.designpatterns.core.usuario.Usuario;
import org.springframework.stereotype.Component;

@Component
public final class SmsNotification implements Notification {
    public void notificate(Usuario usuario, String mensagem) {
        System.out.printf("Enviando SMS para %s (%s): %s%n",
                usuario.nome(), usuario.telefone(), mensagem);
    }
}

Podemos agora testar nossa aplicação e validar o envio das notificações de email e sms quando um usuário e cadastrado.

Image description

Image description

Este projeto demonstra como combinar design patterns clássicos com os novos recursos das versoes mais recentes do Java, usando um sistema simples de cadastro de usuários com notificações via e-mail e SMS.

🔧 Padrões Utilizados

🔒 Singleton

O padrão Singleton foi aplicado com o uso do Spring Boot para garantir que o serviço UsuarioService tenham apenas uma instância durante todo o ciclo de vida da aplicação.

🏭 Factory

Utilizamos uma NotificationFactory para encapsular a criação das instâncias de notificações (EmailNotification e SmsNotification) com base em um tipo definido na escolha do usuario.

👀 Observer

O padrão Observer permite que diferentes canais de notificação sejam observadores do evento de cadastro de usuário. Assim, sempre que um novo usuário é registrado, todos os canais registrados recebem a notificação.

🆕 Features das versoes recentes do Java

  • sealed interface: restringe quais classes podem implementar uma interface, trazendo mais segurança e controle ao design.
  • Pattern matching com instanceof: permite validações de tipo mais limpas e seguras.
  • switch com enum: possibilita o tratamento completo dos tipos permitidos com checagem de exaustividade pelo compilador.

📦 Classes Principais

  • Usuario: representa o modelo de dados do usuário, contendo informações como nome e preferência de notificação (e-mail ou SMS).

  • Notification: interface selada (sealed interface) que define o contrato para os tipos de notificação. Ela é implementada por:

    • EmailNotification: responsável por enviar notificações por e-mail.
    • SmsNotification: responsável por enviar notificações via SMS.
  • NotificationFactory: aplica o padrão Factory, sendo responsável por fornecer a instância correta de Notification com base na preferência informada pelo usuário (por exemplo, "email" ou "sms").

  • NotificationService: atua como publisher de eventos no padrão Observer moderno via ApplicationEventPublisher, enviando eventos de forma desacoplada para serem tratados por listeners.

  • UsuarioService: encapsula a lógica de cadastro de usuários e é responsável por acionar a notificação através da publicação de um evento (UsuarioCadastradoEvent) que será capturado posteriormente pelo listener.

  • UsuarioEventListener: classe que observa os eventos de usuários cadastrados. Utiliza pattern matching com switch para tratar dinamicamente o tipo de notificação a ser enviada, de forma segura e moderna.

  • UsuarioCadastradoEvent: classe que representa o evento de domínio publicado após o cadastro de um novo usuário. Contém os dados do usuário para consumo pelos observers.

✅ Conclusão

A combinação dos padrões de projeto com os novos recursos das recentes versões do Java trouxe maior robustez, legibilidade e extensibilidade ao projeto. Essa abordagem é ideal para arquiteturas modernas e escaláveis.


O projeto está disponível no GitHub. Sua estrela é muito bem-vinda se você achou o conteúdo relevante:
🔗 DesignPatterns
Desenvolvido para fins educacionais por Hernani Almeida