Ir para o conteúdo principal
Background Image

Princípio "Open-Closed Principle" do S.O.L.I.D. na prática

··8 minutos·
Rafael Issao
Autor
Rafael Issao
Apaixonado por tecnologia, programação e inovação. Adoro compartilhar conhecimento e aprender coisas novas todos os dias.
Tabela de conteúdos
SOLID na prática - Este artigo faz parte de uma série de artigos.
Parte 2: Esse Artigo

Programação Orientada à Objetos é um paradigma difícil de dominar.

Quando aprendemos a programar de forma procedural na faculdade e nos cursos, criamos um modelo mental que facilita a escrita de um código que vai contra as ideias por trás de POO.

Muitas faculdades e cursos ensinam polimorfismo e abstração sem explicar o problema que estes conceitos ajudam a nos atacar quando programamos de forma procedural.

Neste artigo, gostaria de explorar um dos problemas que os desenvolvedores enfrentavam antes do paradigma orientada à objetos e como o “Open-Closed Principle” junto com POO contribui para a solução do problema.

Problema: Uma modificação de comportamento era custoso
#

Imagine um sistema simples onde a função é imprimir a informação da pessoa cadastrada:

def pessoa = [
    nome: 'Rafael Issao',
    idade: 18
]

void imprimeInfoPessoaNoTerminal(pessoa) {
    println "${pessoa.nome} ${pessoa.idade}"
}

void imprimeInfoPessoaNoArquivo(pessoa) {
    new File('infoPessoa').text = "${pessoa.nome} ${pessoa.idade}"
}

Este sistema imprime o nome e a idade da pessoa no terminal e no arquivo.

Mas nós sabemos que um sistema sempre está em constante mudança.

Por exemplo, o que acontece quando precisamos adicionar a data de nascimento?

Precisamos alterar três lugares:

def pessoa = [
    nome: 'Rafael Issao',
    idade: 18,
    dataNascimento: '01/01/2025'
]

void imprimeInfoPessoaNoTerminal(pessoa) {
    println "${pessoa.nome} ${pessoa.idade} ${pessoa.dataNascimento}"
}

void imprimeInfoPessoaNoArquivo(pessoa) {
    new File('infoPessoa').text = "${pessoa.nome} ${pessoa.idade} ${pessoa.dataNascimento}"
}

O custo de alteração é alto. Tivemos que mexer em todas as funções e estrutura de dados.

Isso era recorrente em programação procedural. A dificuldade de manter os dados e a lógica em um lugar era complicado.

POO surgiu como solução para diminuir esse tipo de custo. A ideia é deixar a lógica e a estrutura de dados em um único lugar, nas classes:

class Pessoa {
    String nome
    int idade

    static Pessoa com(String nome, int idade) {
        return new Pessoa(nome: nome, idade: idade)
    }

    String informacao() {
        return "$nome $idade"
    }
}

Pessoa pessoa = Pessoa.com("Rafael Issao", 18);

void imprimeInfoPessoaNoTerminal(Pessoa pessoa) {
    println pessoa.informacao()
}

void imprimeInfoPessoaNoArquivo(Pessoa pessoa) {
    new File('infoPessoa').text = pessoa.informacao()
}

Se quisermos colocar a data de nascimento, precisamos somente alterar a classe:

class Pessoa {
    String nome
    int idade
    String dataNascimento

    static Pessoa com(String nome, int idade, String dataNascimento) {
        return new Pessoa(nome: nome, idade: idade, dataNascimento: dataNascimento)
    }

    String informacao() {
        return "$nome $idade $dataNascimento"
    }
}

Pessoa pessoa = Pessoa.com("Rafael Issao", 18, "01/01/2025");

void imprimeInfoPessoaNoTerminal(Pessoa pessoa) {
    println pessoa.informacao()
}

void imprimeInfoPessoaNoArquivo(Pessoa pessoa) {
    new File('infoPessoa').text = pessoa.informacao()
}

O custo de alteração é menor e esse é o poder da abstração.

As duas funções responsáveis por imprimir a informação da pessoa depende somente do comportamento Pessoa#informacao().

E aqui mora a primeira confusão fundamental para a maioria dos desenvolvedores. Parece que as funções dependem do objeto tipo Pessoa. Mas não é isso.

As duas funções dependem somente do comportamento informacao(). Ou seja, você pode passar qualquer objeto que tenha o método informacao() que elas continuam funcionando.

Isso significa que posso criar uma outra classe, que tenha o método informacao() e passar para estas funções!

Ou seja, se quisermos imprimir informações de Pessoa Jurídica no nosso sistema, é só criar uma classe que implementa o método informacao():

class PessoaJuridica {
    String razaoSocial
    String cnpj

    String informacao() {
        return "$razaoSocial $cnpj"
    }
}

def pessoa = new PessoaJuridica(razaoSocial: "Youready", cnpj: "50.090.353/0001-06");

void imprimeInfoPessoaNoTerminal(PessoaJuridica pessoa) {
    println pessoa.informacao()
}

void imprimeInfoPessoaNoArquivo(PessoaJuridica pessoa) {
    new File('infoPessoa').text = pessoa.informacao()
}

Essa é a ideia também do “Open-Closed Principle”.

Uma entidade (funções, classes, módulos, etc.) deve estar aberto para extensão mas fechado para modificação.

No nosso exemplo, as funções estão fechadas para modificação mas aberto para extensão. O ponto de extensão é que você pode utilizar qualquer objeto que tenha o método informacao().

Um exemplo com Java
#

Para aplicar o “OCP” precisamos utilizar abstração, polimorfismo e criar um ponto de extensão.

Vamos ver o que acontece com um código que não atende o princípio “OCP” para depois arrumarmos o problema.

Vamos supor que temos um sistema que é possível simular um dia de trabalho de uma equipe ágil:

class EquipeAgil {
    public void simulaUmDiaDeTrabalho() {
        // ... implementation ...
    }
}

Agora, vamos começar a montar o time. Primeiro, vamos adicionar desenvolvedores:

class EquipeAgil {
    private final List<Desenvolvedor> desenvolvedores = new ArrayList<>();

    public void simulaUmDiaDeTrabalho() {
        for (Desenvolvedor desenvolvedor : desenvolvedores) {
            desenvolvedor.desenvolve();
        }
    }

    public void adicionaDesenvolvedor(Desenvolvedor desenvolvedor) {
        desenvolvedores.add(desenvolvedor);
    }
}

class Desenvolvedor {
    public void desenvolve() {
        System.out.println("Desenvolvendo");
    }
}

Percebemos que precisamos de testadores, para ensinar os desenvolvedores a testar e ajudar a equipe a se preocupar com qualidade. Logo, vamos adicionar testadores:

class EquipeAgil {
    private final List<Desenvolvedor> desenvolvedores = new ArrayList<>();
    private final List<Testador> testadores = new ArrayList<>();

    public void simulaUmDiaDeTrabalho() {
        for (Desenvolvedor desenvolvedor : desenvolvedores) {
            desenvolvedor.desenvolve();
        }
        // Para cada novo perfil, preciso adicionar um novo loop
        for (Testador testador : testadores) {
            testador.testa();
        }
    }

    public void adicionaDesenvolvedor(Desenvolvedor desenvolvedor) {
        desenvolvedores.add(desenvolvedor);
    }

    // Para cada novo perfil, preciso criar um método para adiciona
    public void adicionaTestador(Testador testador) {
        testadores.add(testador);
    }
}

class Desenvolvedor {
    public void desenvolve() {
        System.out.println("Desenvolvendo");
    }
}

class Testador {
    public void testa() {
        System.out.println("Testando");
    }
}

Veja o problema aqui. Com o design que temos agora, para cada profissional com novo perfil na equipe, precisamos modificar a classe EquipeAgil.

Ou seja, esta classe não está fechada para modificação. Por consequência, temos um custo maior para alterar o comportamento do nosso código.

Para resolver este problema criamos uma abstração para representar todas as necessidades da classe EquipeAgil.

Vamos chamar, por exemplo, essa abstração de Profissional e implementá-las nas classes Desenvolvedor e Testador.

E também vamos utilizar a abstração Profissional na classe EquipeAgil:

interface Profissional {
    void executa();
}

// Nunca mais precisamos modificar esta classe
class EquipeAgil {
    private final List<Profissional> profissionais = new ArrayList<>();

    public void simulaUmDiaDeTrabalho() {
        // Nao importa o perfil, todos executam
        for (Profissional profissional : profissionais) {
            profissional.executa();
        }
    }

    // Nao importa o perfil, todos sao adicionados por este método
    public void adicionaProfissional(Profissional profissional) {
        profissionais.add(profissional);
    }
}

class Desenvolvedor implements Profissional {
    public void desenvolve() {
        System.out.println("Desenvolvendo");
    }

    @Override
    public void executa() {
        this.desenvolve();
    }
}

class Testador implements Profissional {
    public void testa() {
        System.out.println("Testando");
    }

    @Override
    public void executa() {
        this.testa();
    }
}

O importante é que agora, a classe EquipeAgil não precisa mais ser modificada para adicionar novos profissionais.

Ela tem os dois principais atributos:

  • Aberta para extensão: Para adicionar novos profissionais, basta criar uma classe que implemente a interface Profissional.
  • Fechada para modificação: Não precisamos modificar a classe EquipeAgil para adicionar novos profissionais.

Se quisermos adicionar agora um gerente de produto, o código fica assim:

interface Profissional {
    void executa();
}

class EquipeAgil {
    private final List<Profissional> profissionais = new ArrayList<>();

    public void simulaUmDiaDeTrabalho() {
        for (Profissional profissional : profissionais) {
            profissional.executa();
        }
    }

    public void adicionaProfissional(Profissional profissional) {
        profissionais.add(profissional);
    }
}

class Desenvolvedor implements Profissional {
    public void desenvolve() {
        System.out.println("Desenvolvendo");
    }

    @Override
    public void executa() {
        this.desenvolve();
    }
}

class Testador implements Profissional {
    public void testa() {
        System.out.println("Testando");
    }

    @Override
    public void executa() {
        this.testa();
    }
}

class GerenteDeProduto implements Profissional {
    public void gerencia() {
        System.out.println("Gerenciando");
    }

    @Override
    public void executa() {
        this.gerencia();
    }
}

Legal!

A abstração é o Profissional, o polimorfismo é feito com as classes concretas que implementam a interface Profissional e o ponto de extensão é pelo método adicionaProfissional(). Qualquer objeto que implementa a interface Profissional pode ser utilizada no sistema.

Uma observação sobre polimorfismo
#

Polimorfismo é um atributo que facilita a aplicação do “OCP”.

Existem algumas maneiras de atingir o polimorfismo em linguagens com tipagem estática como Java e C#:

  • Interface
  • Herança

Se estiverem em dúvida em qual utilizar, sempre utilize interface.

Só use herança se tiver uma absoluta certeza do que você está fazendo.

Explicarei o motivo desta observação quando explicar o “L” do S.O.L.I.D.

Mas para linguagens com tipagem dinâmica como Groovy, Ruby e Javascript, não precisamos nem de interface e nem de herança. Basta implementar uma função ou método com mesmo nome e parâmetros.

A vantagem de linguagens com tipagem estática é que o contrato para aplicação do OCP fica mais claro. Mas é necessário mais código.

Em linguagens com tipagem dinâmica é só implementar o método, mas o código fica mais complexo para entender a abstração, polimorfismo e o ponto de extensão do “OCP”.

Então, entender as funcionalidades da linguagem que você está trabalhando se torna importante para a aplicação do OCP.

Pense em como você quer transmitir o ponto de extensão em cada situação para sua equipe e para futuros desenvolvedores que irá trabalhar com o projeto.

Conclusão
#

“OCP” é um princípio importante em POO que lembra você a criar um design do seu software com um custo menor de alteração.

Entender bem abstração, polimorfismo e ponto de extensão ajuda a você manter o design com “OCP” em dia!

O que você achou do artigo?

Se tiver mais curiosidade sobre o assunto, pode falar comigo!

🥒🥒 Espero que tenham curtido e até o próximo artigo! 🥒🥒

SOLID na prática - Este artigo faz parte de uma série de artigos.
Parte 2: Esse Artigo