O Abstract Factory é um padrão de projeto do tipo criacional que oferece uma solução eficiente para criar uma família de objetos relacionados, sem especificar as classes concretas. Em outras palavras, ele permite criar grupos de objetos através de abstrações/ interfaces. Tudo isso, garantindo que os objetos sejam compatíveis entre si.

Imagine que você está desenvolvendo um sistema para uma escola e ela tenha diferentes níveis de ensino, como ensino fundamental e ensino médio. E que, cada nível precise de um conjunto específico de objetos relacionados como, alunos, professores e materiais didáticos. Esses objetos têm características distintas dependendo do nível de ensino. Por exemplo:

Alunos do fundamental podem ter atributos como "responsável legal" e alunos do ensino médio não terão este atributo.
Professores do ensino médio podem ter especializações específicas, ao contrário do ensino fundamental.
Materiais didáticos variam conforme o nível (livros infantis no fundamental e livros técnicos no Médio).

Com o Abstract Factory, podemos criar uma fábrica abstrata que terá métodos para criar as famílias dos objetos relacionados (aluno, professor e material didático). Cada implementação concreta da fábrica será responsável por criar objetos específicos para cada nível de ensino.

Façamos um comparativo entre uma implementação inicial sem padrão definido e uma com o padrão Abstract Factory.

Sem padrão definido

Criei uma classe para Pessoa. De forma que eu reaproveite código entre Aluno e Professor.

public class Pessoa {
    private String nome;
    private String sobrenome;
    private String email;

    public Pessoa(String nome, String sobrenome, String email) {
        this.nome = nome;
        this.sobrenome = sobrenome;
        this.email = email;
    }
}

Criei uma classe para Aluno, que terá o atributo responsavelLegal

@Getter
@Setter
public class Aluno extends Pessoa {

    private Pessoa responsavelLegal;

    public Aluno(String nome, String sobrenome, String email) {
        super(nome, sobrenome, email);
    }

}

Criei a classe Professor que terá mais dois atributos adicionais

@Getter
@Setter
public class Professor extends Pessoa {

    private String disciplina;
    private String especializacao;

    public Professor(String nome, String sobrenome, String email) {
        super(nome, sobrenome, email);
    }

}

Criei uma classe para MaterialDidatico

@Getter
@Setter
@AllArgsConstructor
public class MaterialDidatico {
    private String livro;
    private BigDecimal preco;
}

Desta forma, quando a classe cliente precisar matricular um aluno, deverá conhecer a implementação de cada objeto.

public class EscolaService {

    public void matricularAluno(){
        Pessoa responsavel = new Pessoa("responsavel", "legal", "email2");

        Aluno aluno = new Aluno("fulano","silva", "email1");
        aluno.setResponsavelLegal(responsavel);

        Professor professor = new Professor("professor", "silva", "email3");
        professor.setDisciplina("Geral");

        MaterialDidatico materialDidatico = new MaterialDidatico("livro I", new BigDecimal(100));

        ///restante da implementação

    }    
}

Esta prática, embora simples, pode tornar a classe cliente extremamente grande, complexa, acoplada, difícil de manter, entender e escalar.
Por outro lado, a implementação do Abstract Factory nesse exemplo pode resolver todos estes problemas. Acompanhe...:

Com Abstract Factory

Criei uma interface para criar os objetos

public interface EnsinoFactory {

    Aluno criarAluno(String nome, String sobrenome, String email, String responsavel);

    Professor criarProfessor(String nome, String sobrenome, String email, String disciplina, String especializacao);

    MaterialDidatico criarMaterialDidatico();
}

E uma classe concreta para o ensino fundamental. Aqui, implementei uma regra exigindo que o responsável legal seja populado para o Aluno e ignorei a especialização do professor. Por outro lado, o material didático já possui suas próprias regras, desprezando parâmetros de entrada.

public class EnsinoFundamentalFactory implements EnsinoFactory {

    @Override
    public Aluno criarAluno(String nome, String sobrenome, String email, String responsavel) {
        if (responsavel == null || responsavel.isEmpty()) {
            throw new IllegalArgumentException("Responsável não pode ser nulo ou vazio");
        }
        return new Aluno(nome, sobrenome, email, responsavel);
    }

    @Override
    public Professor criarProfessor(String nome, String sobrenome, String email, String disciplina, String especializacao) {
        return new Professor(nome, sobrenome, email);
    }

    @Override
    public MaterialDidatico criarMaterialDidatico() {
        return new MaterialDidatico("Livro Infantil I", new BigDecimal("100.00"));
    }
}

O mesmo sobre a classe concreta do ensino médio. Aqui garanti que a especialização do professor seja informada. Desconsiderei o responsável do aluno e já criei o material didático com suas especificidades.

public class EnsinoMedioFactory implements EnsinoFactory {

    @Override
    public Aluno criarAluno(String nome, String sobrenome, String email, String responsavel) {
        return new Aluno(nome, sobrenome, email);
    }

    @Override
    public Professor criarProfessor(String nome, String sobrenome, String email, String disciplina, String especializacao) {
        if (especializacao == null || especializacao.isEmpty()) {
            throw new IllegalArgumentException("Especialização não pode ser nula ou vazia");
        }
        if (disciplina == null || disciplina.isEmpty()) {
            throw new IllegalArgumentException("Disciplina não pode ser nula ou vazia");
        }
        return new Professor(nome, sobrenome, email, disciplina, especializacao);
    }

    @Override
    public MaterialDidatico criarMaterialDidatico() {
        return new MaterialDidatico("Material de Ensino Médio", new BigDecimal("150.00"));
    }
}

Agora, a classe cliente chamará a factory e terá acesso aos métodos em específico

public class EscolaService {

    EnsinoFactory factoryFundamental = new EnsinoFundamentalFactory();
    EnsinoFactory factoryMedio = new EnsinoMedioFactory();

    public void matricularAluno(){
        //Criando um aluno do ensino fundamental
        Aluno aluno = factoryFundamental.criarAluno("josé","Silva","[email protected]","Maria");
        Professor professor = factoryFundamental.criarProfessor("Mauricio", "Souza", "prof@email",null, null);
        MaterialDidatico materialDidatico = factoryFundamental.criarMaterialDidatico();

        //Criando um aluno do ensino médio
        Aluno alunoMedio = factoryMedio.criarAluno("Bia","Carvalho","[email protected]",null);
        Professor professorMedio = factoryMedio.criarProfessor("Ana", "Lima", "prof@email","Portugues", "Literatura");
        MaterialDidatico materialDidaticoMedio = factoryMedio.criarMaterialDidatico();
        ///Restante do código
    }
}

Agora você pode estar pensando se valeu a pena, não é mesmo!?
Suponhamos que amanhã surja o ensino técnico, e ele tenha suas próprias regras. Com esta implementação, fica fácil criar uma implementação nova para ele sem alterar as demais. Desta forma, deixamos as regras separadas por cada tipo de matrícula, diminuímos a quantidade de código na classe cliente e, com isso, baixamos a complexidade, risco de bugs e aumentamos a facilidade em manter, entender e escalar o código.

Dá pra fazer diferente?

Como em todos os padrões, a implementação pode variar em cima das necessidades. A ideia é ajudar, mas nunca, engessar.

Neste exemplo poderíamos ainda, criar interfaces para Aluno e Professor. Apesar de incrementar mais classes, isso deixaria o código ainda mais desacoplado e fácil de manter.

public interface Aluno {
String getNome();
// outros métodos comuns
}

public class AlunoFundamental implements Aluno { ... }
public class AlunoMedio implements Aluno { ... }

Outro ajuste seria utilizar a sobrecarga de métodos na interface EnsinoFactory. Ao invés de termos um método criarAluno() com todos os parâmetros, poderíamos ter dois. Um com os parâmetros do fundamental e outro com os parâmetros do médio. Desta forma não precisaríamos passar parâmetros desnecessários da classe cliente.

public interface EnsinoFactory {
  Aluno criarAluno(String nome, String sobrenome, String email, String responsavelLegal); // Fundamental
  Aluno criarAluno(String nome, String sobrenome, String email); // Médio

Uma terceira possibilidade seria criar uma classe "Matricula", que recebesse Aluno, Professor e Matricula. Depois retornar este objeto em cada factory. Desta forma, ao criar uma matricula os 3 objetos sempre seriam retornados juntos.

Veja que, existem diversas formas de se pensar e mesmo assim se basear no mesmo padrão. Tudo depende do contexto e necessidade de cada cenário.

Vantagens

Desacoplamento: o cliente trabalha apenas com interfaces/abstrações, sem conhecer as classes concretas dos produtos.

Centralização das regras de negócio: as regras específicas de cada família ficam encapsuladas nas fábricas concretas, facilitando manutenção e evolução.

Facilidade para adicionar novas famílias: para implementar uma nova variação como o Ensino Técnico, por exemplo, basta criar uma nova fábrica concreta e suas implementações, sem alterar código existente (princípio Open/Closed).

Testes: facilita a criação dos testes unitários, pois é possível injetar diferentes fábricas em cenários de teste.

Desvantagens

Complexidade: aumenta o número de classes no projeto, principalmente se houver muitas famílias.

Custo: Se as diferenças entre famílias forem mínimas, pode ser um “overkill” usar o padrão, pois demanda maior esforço para implementa-lo.

Tiro pela culatra: se precisar adicionar um novo tipo de objeto à família, como coordenador por exemplo, será necessário modificar todas as fábricas abstratas e concretas, violando o princípio Open/Closed nesse aspecto.

Curva de aprendizado: pode ser mais difícil de entender a princípio.

Entenda também