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! 🥒🥒