DEV Community

Cover image for Orientação a Objetos
Lucas Mateus
Lucas Mateus

Posted on

Orientação a Objetos

Olá! Hoje vamos ver um pouco sobre orientação a objetos, abordando os seguintes tópicos:

  • 1. Introdução
    • 1.1 História
    • 1.2 Definição
    • 1.3 Estrutura
    • 1.3.1 Classes x Objetos
  • 2. Características
    • 2.1 Alocação de memória dinâmica
    • 2.2 Encapsulamento
    • 2.3 Herança
    • 2.4 Polimorfismo
  • 3. Problemas na OO
    • 3.1 Acoplamento e coesão inadequados
    • 3.2 Herança excessiva
    • 3.3 Curva de aprendizagem
    • 3.4 Sobrecarga de memória
  • 4. Quando escolher
  • 5. Conclusão

Espero que você goste e qualquer feedback é só mandar na área de comentários :)

=================================================

1.Introdução

1.1 História

Caso você não queira saber sobre a história (o que considero importante), pode pular para o item 1.2. Mas, caso queira se aprofundar mais, venha comigo.

A OO surgiu na década de 1960 - faz tempo pra caramba e, ao mesmo tempo, não (risos) - com dois pesquisadores do Centro Norueguês de Computação, Kristen Nygaard e Ole-Johan Dahl, que iniciaram um projeto buscando elaborar uma linguagem de Simulação de Eventos Discretos.

"O que raios é Simulação de Eventos Discretos?" é um dos modelos matemáticos usados para descrever como o computador compreende a lógica dos eventos diários, mas esse não é o foco agora. Vamos continuar.

Eles conseguiram desenvolver o que pretendiam criar, dando origem a SIMULA, que passou por atualizações e se tornou a SIMULA 67. Até hoje, é a mãe do paradigma orientado a objetos.

Mas calma, os conceitos que você virá a conhecer não vieram diretamente dela, mas sim com a criação da Smalltalk-80, que inspirou linguagens como C++, Java, C# etc.

Tudo bem, não vou cansá-lo muito sobre isso. Precisamos ir para as características da orientação a objetos.

1.2 Definição

A Orientação a Objetos (OO) possui inúmeras definições, algumas muito complexas, outras simples, porém muito superficiais em termos conceituais e assim por diante. Todas as definições são semelhantes em algum ponto. Seguem algumas definições:

"Orientação a objetos é uma forma de decompor sistemas complexos em módulos independentes, que podem ser compreendidos e modificados separadamente. Cada módulo é responsável por uma parte do sistema e se comunica com os outros módulos por meio de interfaces bem definidas. Essa abordagem permite que o código seja organizado de forma modular e reutilizável, facilitando a manutenção e evolução do sistema ao longo do tempo."

Uncle Bob

Antes de prosseguirmos, é importante salientar que se uma pessoa iniciante no mundo da programação ler essa definição, talvez surjam algumas perguntas, como "o que é um módulo?", "o que são interfaces? É aquilo que eu posso ver e digitar como um aplicativo de finanças?", "o que é código reutilizável?" e assim por diante. Meu ponto é que, embora essa definição seja rica, ela pode não ser tão amigável para quem está iniciando na área. Vamos continuar com outra definição.

A Orientação a Objetos é um paradigma de programação que se baseia na composição de objetos, cada um com propriedades e comportamentos que interagem entre si para atingir um objetivo. Essa abordagem tem como objetivo modelar o mundo real de forma mais próxima, permitindo a construção de sistemas mais flexíveis, reutilizáveis e fáceis de manter.

Definição acadêmica

Essa definição é mais apropriada para iniciantes, mas ainda há alguns pontos que precisam ser esclarecidos, como "o que são objetos?", "o que é um sistema flexível?", "o que é um paradigma?". Dito isso, tenho uma visão que não é tão rica, mas acredito que passa bem o que é a orientação a objetos como definição.

"A Orientação a objetos é um estilo de programação baseado na centralização de dados e comportamentos, com o intuito de facilitar a construção e a manutenção de um sistema ao longo do tempo, além de buscar aproximar o código da realidade."

Lucas Jdev

Em minha concepção, essa definição não é tão elegante ou rica quanto a do Uncle Bob, mas é simples e consegue transmitir com sucesso seu objetivo, que é explicar de maneira didática o que é a orientação a objetos.

1.3 Estrutura

É bem possível que você, como iniciante, não tenha entendido o que eu quis dizer. Então, para esclarecer, vamos apresentar a estrutura de um código orientado a objetos. Neste post, utilizarei a linguagem Java.

1.3.1 Classes x Objetos

Classes são locais onde centralizamos dados e comportamentos. Uma classe é uma abstração de modelo para objetos. Mas o que isso significa? Imagine que você precise transformar em código uma modelagem simplificada de um carro para um simulador de corridas de Fórmula 1. Considere que um carro possui características como nome, cor, ano e velocidade. A modelagem poderia ser assim:

classe_carro_atributos

E em código assim:

class Carro{
   String nome;
   String cor;
   int ano;
   double velocidade;
}
Enter fullscreen mode Exit fullscreen mode

"Pronto, criamos nossa classe Carro, mas há algo que precisa ser adicionado. A classe atual não possui comportamentos/métodos, e sabemos que na realidade um carro possui diversas ações, como acelerar e frear. Vamos implementar esses métodos agora."

modelagem UML:
classe_carro_completa

modelagem em código:

class Carro{
   String nome;
   String cor;
   int ano;
   double velocidade;

   void acelerar(double variacaoDeVelocidade){
      velocidade = velocidade + variacaoDeVelocidade;
   }

   void frear(double variacaoDeVelocidade){
      velocidade = velocidade - variacaoDeVelocidade;
   }

}
Enter fullscreen mode Exit fullscreen mode

Objetos são instâncias criadas a partir de suas respectivas classes. "Não entendi nada!", pense comigo, se você precisar criar um carro, uma instância, como você faz isso? "Não faço ideia". Segue o fio aqui:

solução UML:
objeto_carro

solução em código:

class Carro{
   String nome;
   String cor;
   int ano;
   double velocidade;

   void acelerar(double variacaoDeVelocidade){
      velocidade = velocidade + variacaoDeVelocidade;
   }

   void frear(double variacaoDeVelocidade){
      velocidade = velocidade - variacaoDeVelocidade;
   }

}

public class Teste{
   public static void main(String[] args){
      Carro carro_1 = new Carro();
      carro_1.nome = "Corola U330";
      carro_1.cor = "Branco";
      carro_1.ano = 2019;
      carro_1.velocidade = 0;

      Carro carro_2 = new Carro();
      carro_2.nome = "Fiat Uno 1.0";
      carro_2.cor = "Vermelho";
      carro_2.ano = 2022;
      carro_2.velocidade = 80.5;
   }
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos acelerar o carro_1 e desacelerar o carro_2.

// ...
carro_1.acelerar(20); // acelera em 20 unidades (km/h ou m/s)
carro_2.frear(40); // desacelera em 40 unidades (km/h ou m/s)
Enter fullscreen mode Exit fullscreen mode

Provavelmente você notou que para acessar qualquer atributo ou método, utiliza-se variavel.oqueQuerAcessar. Mais adiante, vou explicar como podemos restringir algumas coisas ;)

2. Características

2.1 Alocação de memória dinâmica

Para falar sobre isso, primeiro veja:

fluxo_alocacao

Na programação orientada a objetos, a alocação dinâmica de memória é um recurso utilizado para alocar espaço na memória durante a execução do programa. Em outras palavras, é uma maneira de alocar a quantidade exata de memória que o programa precisa, quando ele precisa.

Isso é diferente da alocação estática de memória, que aloca uma quantidade fixa de memória durante a compilação do programa. Com a alocação dinâmica, o programador pode solicitar a alocação de um bloco de memória de tamanho variável em tempo de execução, o que pode ser muito útil para lidar com situações em que o tamanho da memória necessário não é conhecido antecipadamente ou pode variar.

2.1.1 Valor x Referência

Uma coisa muito importante é que a forma como os dados são armazenados na memória é diferente para objetos primitivos e não primitivos. "Como assim?" Observe:

public class FormaPrimitiva {
   public static void main(String[] args) {
       int x = 5;
       int y = x;
       y = 7;

       System.out.println(x); // saída: 5
   }
}
Enter fullscreen mode Exit fullscreen mode

Na memória o salvamento ocorre assim:

alocacao_por_valor

"E como é com objetos não primitivos?", considere os seguintes códigos:

class Cliente{
   String nome;
   int idade;
}

public class FormaNaoPrimitiva{
   public static void main(String[] args){
      Cliente objeto_1 = new Cliente();
      Cliente objeto_2 = objeto_1;
   }
}
Enter fullscreen mode Exit fullscreen mode

Na memória ocorre dessa forma:

alocacao_por_ref

Essas formas possuem nomes específicos chamados atribuição por valor e por referência, respectivamente. No modo primitivo, atribuímos um valor diretamente ao objeto. Em outras palavras, o que fizermos na variável y não afetará a variável x. Portanto, esse tipo de atribuição não promove efeito colateral no objeto posterior. No entanto, a atribuição por referência nos traz um efeito colateral grave, pois ambos os objetos compartilham a mesma instância na memória. Esse recurso visa a economia de recursos, portanto é de extrema importância tomar cuidado com isso durante o desenvolvimento de software.

"Como assim efeito colateral?", observe:

public class FormaNaoPrimitiva{
   public static void main(String[] args){
      Cliente objeto_1 = new Cliente();
      Cliente objeto_2 = objeto_1;
      objeto_2.nome = "Lucas";

      System.out.println(objeto_1.nome); // saída: Lucas
   }
}
Enter fullscreen mode Exit fullscreen mode

Sendo assim, tome muito cuidado com esse tipo de situação e se possível, evite a depender do caso.

2.2 Encapsulamento

De forma sintetizada, encapsulamento é um conceito da OO que baseia-se em ocultar os detalhes internos dos objetos, fornecendo uma "interface" clara para o sistema.

Se você não entendeu, observe o código:

class Cliente{
   private String nome;
   private int idade;

   public void mudaNome(String novoNome){
      if (novoNome != null || novoNome.isBlank()){
         nome = novoNome;
      }
   }

   public void mudaIdade(int novaIdade){
      if (novaIdade > 0){
         idade = novaIdade;
      }
   }
}
Enter fullscreen mode Exit fullscreen mode

Perceba que aumentamos a quantidade de detalhes para a classe cliente, mas se formos acessar o que ela tem em uma classe de teste - não estou falando necessariamente de testes com junit -, será fácil acessar, porém haverá restrições quanto aos dados passados. Faz sentido, na realidade, passar um valor negativo para a idade? Faz sentido eu passar um valor nulo para um nome? Se buscamos aproximar o código da realidade, precisamos delimitar os contextos como acontece na vida real.

"O que é esse public e private?" Bom, visando o encapsulamento na linguagem Java, alguns conceitos simples precisam estar claros na sua cabeça:

encapsulamento

Logo, privatizamos o acesso direto a variável e acessamos por meio de métodos públicos contendo restrições de maneira interna.

OBS: Linguagens modernas como python e typescript possuem uma flexibilidade de fazer um set privado que funciona para acesso direto a variável.

tabela_pros_e_contras_encapsulamento

2.3 Herança

Herança é um dos conceitos mais complexos, apesar de ser simples de entender. Pense comigo: na vida real, a herança é algo que é passado de pais para filhos, e algumas características são transmitidas de geração em geração. Na programação, seguimos um princípio semelhante, como diz o ditado "filho de peixe, peixinho é".

Partindo-se desta perspectiva, vamos pensar na seguinte modelagem:

  • Toda pessoa possui: nome e idade;
  • Toda pessoa física possui: nome, idade e cpf;
  • Toda pessoa jurídica possui: nome, idade e cnpj.

Como poderíamos modelar isso? Talvez fazendo uma classe para cada um com os mesmos atributos, mas... isso feriria o DRY (Don't Repeat Yourself), então para isso eu proponho a seguinte modelagem:

UML:

pessoa_uml

código:

public class Pessoa {
   private String nome;
   private int idade;

   public Pessoa(String nome, int idade){
      this.nome = nome;
      this.idade = idade;
   }

   public String pegaNome(){
      return nome;
   }

   public int pegaIdade(){
      return idade;
   }
}

public class PessoaFisica extends Pessoa {
   private String cpf;

   public PessoaFisica(String nome, int idade, String cpf){
      super(nome, idade);
      this.cpf = cpf;
   }

   public int pegaCPF(){
      return cpf;
   }
}

public class PessoaJuridica extends Pessoa {
   private String cnpj;

   public PessoaJuridica(String nome, int idade, String cnpj){
      super(nome, idade);
      this.cnpj = cnpj;
   }

   public int pegaCNPJ(){
      return cnpj;
   }
}
Enter fullscreen mode Exit fullscreen mode

Vamos analisar esse código com calma. Observe que agora temos algumas coisas que não foram apresentadas antes como extends, contrutores e o super.

"o que são essas coisas?", extends é o termo usado para "herdar de" ou "extende de". Isso, simplesmente isso. "O que é super e esse tal de construtores?", super é a forma como se referimos as classes do topo da hierarquia, ou seja, é a maneira que referenciamos superclasses. Já os construtores são uma forma de você definir uma regra de inicialização do objeto. "Como assim?", bem... em todos os casos de instanciação ao longo deste post, vc viu que inicializávamos com o new ClasseReferencia(), agora iremos instanciar assim: new ClasseReferencia(argumentos). Exemplo:

public class Testador {
   public static void main(String[] args){
      PessoaFisica pf = new PessoaFisica("Fulano", 23, "256.774.350-22");

      PessoaJuridica pj = new PessoaJuridica("Ciclano", 21, "99.274.837/0001-92");

      Pessoa p = new Pessoa("Beltrano", 19);
   }
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, não precisamos fazer variavel.algumaCoisa para inicializar o valor dos atributos, podemos inicializar de maneira direta.

Uma coisa muito curiosa que talvez quem já programe em Java rotineiramente por um tempo percebe é que "nunca" delimitamos nossas heranças.
"Quê??", Isso mesmo, nós não nos preocupamos com isso, porém isto abre uma brecha enorme para heranças sem sentido. Exemplo:

public sealed class FormaGeometrica permits Circulo, Retangulo {
    protected double largura, altura;

    protected FormaGeometrica(double largura, double altura) {
        this.largura = largura;
        this.altura = altura;
    }

    public abstract double area();
}

final class Circulo extends FormaGeometrica {
    private int raio;

    public Circulo(int raio) {
        this.raio = raio;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

final class Retangulo extends FormaGeometrica {

    public Retangulo(int largura, int altura) {
        super(largura, altura);
    }

    @Override
    public double area() {
        return super.largura * super.altura;
    }
}

final class Fractal extends FormaGeometrica {
    private int base, altura;

    public Fractal(int base, altura) {
        super(base, altura);
    }

    @Override
    public double area() {
       // implementação complexa ...
   }

}
Enter fullscreen mode Exit fullscreen mode

Esse caso é muito intrigante porque um Fractal não é uma forma geométrica, logo ele só deve ter acesso à herança se a superclasse permitir. Uma analogia a isso é que você só pode herdar os bens de alguém da sua família se essa pessoa permitir. O erro nesse exemplo ocorrerá em tempo de compilação, portanto, é importante corrigir o erro para que o programa possa ser executado corretamente.

Ah, uma coisa que você ainda não tinha visto é o abstract. Na verdade, ele serve para obrigar uma classe a implementar uma determinada lógica em seus métodos abstratos. Caso seja usado na declaração da classe, o abstract serve para dizer que ela não poderá ser instanciada, apenas declarada. "Não entendi nada!!", então se liga no exemplo:

abstract sealed class FormaGeometrica permits Circulo {
    public abstract double area();
}

final class Circulo extends FormaGeometrica {
    private int raio;

    public Circulo(int raio) {
        this.raio = raio;
    }

    @Override
    public double area() {
        return Math.PI * raio * raio;
    }
}

public class Testador {
   public static void main(String[] args){
      // forma errada
      FormaGeometrica forma_1 = new FormaGeometrica(1,1);

      // forma certa
      FormaGeometrica forma_2 = new Circulo(2);
   }
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, fica evidente que quando usamos abstract na declaração da classe, somos obrigados a instanciar pelo menos uma classe filha.

OBS: O final quando declarado em classe quer dizer que não haverá mais ninguém depois dele na hierarquia, já quando declarado em atributo quer dizer que é uma constante.

Portanto, podemos ver o quão complexo e simples é herança. A definição é simples, mas a depender da situação a implementação pode ser bem complexa.

tabela_pros_e_contras_heranca

2.4 Polimorfismo

Você já teve um prelúdio de como funciona o polimorfismo, entretanto, talvez ainda não saiba o que é. De maneira sucinta, o polimorfismo é a ideia de poder tratar um objeto de variadas formas. "Calma... quê???", isso mesmo, é a ideia de observar as coisas por um referencial mais genérico, permitindo a manipulação de objetos de diferentes classes de forma transparente.

Exemplo:

modelagem_padrao_repository

No desenvolvimento do dia-a-dia você irá se deparar com o padrão repository criado por Erick Evans, então decidi para te mostrar como seria uma leitura disso na prática abordando o polimorfismo e seu poder.

Nesse exemplo, temos duas classes que implementam uma mesma interface. Uma interface é como um contrato, onde define-se um conjunto de métodos que uma classe deve implementar. No caso, a interface RepositorioUsuario define um conjunto de métodos que qualquer classe que a implemente deve ter, ou seja, é um contrato que garante que essas classes terão um conjunto de funcionalidades em comum. Dessa forma, podemos ter uma aplicação que utilize essas classes de forma genérica, sem se preocupar com a implementação específica de cada uma delas, facilitando assim a manutenção e flexibilidade do código.

Partindo dessa reflexão, é compreensível que a abordagem apresentada nos mostre que podemos ver um DAO como um repositório. Se você precisa de um exemplo de código para entender melhor, veja o seguinte:

public interface RepositorioUsuario {
    public void save(Usuario usuario);
    public void delete(Usuario usuario);
    public void update(Usuario usuario);
    public Usuario findById(int id);
}

public class UsuarioOracleDAO implements RepositorioUsuario {
    private Connection connection;

    public UsuarioOracleDAO(Connection connection) {
        this.connection = connection;
    }

    public void save(Usuario usuario) {
        // Lógica para salvar usuário no banco Oracle
    }

    public void delete(Usuario usuario) {
        // Lógica para deletar usuário do banco Oracle
    }

    public void update(Usuario usuario) {
        // Lógica para atualizar usuário no banco Oracle
    }

    public Usuario findById(int id) {
        // Lógica para buscar usuário por ID no banco Oracle e retornar como objeto Usuario
    }

    private Connection createConnection() {
        // Lógica para criar conexão com o banco Oracle
    }
}

public class UsuarioMongoDAO implements RepositorioUsuario {
    private Connection connection;

    public UsuarioMongoDAO(Connection connection) {
        this.connection = connection;
    }

    public void save(Usuario usuario) {
        // Lógica para salvar usuário no banco Mongo
    }

    public void delete(Usuario usuario) {
        // Lógica para deletar usuário do banco Mongo
    }

    public void update(Usuario usuario) {
        // Lógica para atualizar usuário no banco Mongo
    }

    public Usuario findById(int id) {
        // Lógica para buscar usuário por ID no banco Mongo e retornar como objeto Usuario
    }

    private Connection createConnection() {
        // Lógica para criar conexão com o banco Mongo
    }
}
Enter fullscreen mode Exit fullscreen mode

Na classe que contém o método main:

public class Testador(){
   public static void main(String[] args){
      RepositorioUsuario repositorio = new UsuarioMongoDAO(new MongoConnection());

      // ou...

      RepositorioUsuario repositorio = new UsuarioOracleDAO(new OracleConnection());

   }
}
Enter fullscreen mode Exit fullscreen mode

"Ei, eu acho que já vi isso em herança", SIM!! Foi mostrado no tópico anterior, pois tanto para classes quanto para interfaces, podemos usar o polimorfismo. "Ah, então qual é o melhor a usar?", depende do contexto.

O polimorfismo é considerado por muitos o "trunfo" da orientação a objetos, pois permite que objetos de diferentes classes possam ser tratados de maneira uniforme.

tabela_pros_e_contras_polimorfismo

3. Problemas na OO

3.1 Acoplamento inadequado

Acoplamento e coesão são dois conceitos importantes na programação orientada a objetos que estão relacionados com a qualidade do código e a facilidade de manutenção e evolução do software. Quando o acoplamento e a coesão são inadequados, o código se torna mais complexo e difícil de ser entendido e modificado. Vamos ver como isso pode acontecer em uma situação do dia a dia.

Imagine que você está organizando uma festa de aniversário e contratou uma empresa para cuidar da decoração, do bolo e dos doces. Cada uma dessas áreas é responsabilidade de uma equipe diferente da empresa, mas é importante que elas trabalhem em conjunto para que a festa fique bonita e agradável para os convidados.

No entanto, se essas equipes não se comunicarem bem e não tiverem uma boa coordenação, a decoração pode não combinar com o bolo e os doces, ou pode faltar algum item importante. Isso seria um exemplo de acoplamento inadequado, em que as partes do projeto não estão bem integradas e não trabalham em conjunto de forma eficiente.

Além disso, imagine que cada equipe responsável pela decoração, bolo e doces tenha seus próprios líderes e que cada um deles decida fazer as coisas à sua maneira, sem levar em conta o que as outras equipes estão fazendo. Isso pode levar a uma festa desorganizada e com resultados insatisfatórios, mesmo que cada equipe individualmente tenha feito um bom trabalho. Esse seria um exemplo de falta de coesão, em que as partes do projeto não estão trabalhando em harmonia, com um objetivo comum.

Voltando para a programação, o acoplamento inadequado ocorre quando as classes de um sistema dependem demais umas das outras, tornando difícil modificá-las sem afetar outras partes do sistema. Isso pode levar a um código complexo e difícil de ser mantido e evoluído.

Já a falta de coesão ocorre quando uma classe é responsável por muitas coisas diferentes, tornando difícil entender sua funcionalidade e modificá-la sem afetar outras partes do sistema. Isso pode levar a um código confuso e difícil de ser compreendido e modificado.

Por isso, é importante buscar um equilíbrio entre o acoplamento e a coesão ao projetar e implementar um sistema, de forma que as partes trabalhem em conjunto de maneira eficiente e com objetivos claros e comuns. Dessa forma, o código será mais fácil de ser mantido, evoluído e compreendido, assim como a organização de uma festa de aniversário bem-sucedida e agradável para os convidados.

3.2 Herança excessiva

Na programação orientada a objetos, a herança é uma das principais ferramentas para a construção de sistemas mais complexos. Através dela, é possível criar uma hierarquia de classes, com cada uma herdando as propriedades e métodos de sua classe pai. No entanto, quando usada em excesso, a herança pode se tornar um problema, levando a uma árvore genealógica complexa e difícil de gerenciar.

Imagine uma situação em que você precisa criar um sistema de animais, com diversas subclasses para representar cada tipo de animal. Para isso, você cria a classe Animal, que contém as propriedades e métodos básicos que todos os animais possuem, como nome, idade e emitirSom(). A partir daí, você cria subclasses para cada tipo de animal, como Mamífero, Ave e Réptil. Cada uma dessas subclasses possui propriedades e métodos específicos, como pelo, asas e escamas.

Até aqui, tudo bem. No entanto, imagine que você precisa criar subclasses para cada tipo de mamífero, ave e réptil. Para os mamíferos, você cria classes para Gato, Cachorro, Leão, Tigre, etc. Para as aves, você cria classes para Galinha, Pato, Águia, Falcão, etc. E para os répteis, você cria classes para Cobra, Jacaré, Tartaruga, etc. Cada uma dessas subclasses também possui propriedades e métodos específicos, como cor do pelo, tipo de bico e tamanho da cauda.

Agora, imagine que você precise adicionar uma nova propriedade à classe Animal, como tipo de alimentação. Para isso, você teria que adicionar essa propriedade em todas as subclasses que herdaram de Animal, o que seria uma tarefa extremamente trabalhosa e propensa a erros.

Esse é apenas um exemplo simples, mas que ilustra os problemas que a herança excessiva pode trazer. Quanto mais subclasses você cria, mais complexa se torna a árvore genealógica, tornando o sistema difícil de entender e manter. Além disso, qualquer mudança na classe pai pode afetar todas as subclasses que herdaram dela, o que pode levar a bugs difíceis de encontrar.

Outro problema é que a herança excessiva pode levar a um acoplamento forte entre as classes, ou seja, uma mudança em uma classe pode afetar outras classes que não têm relação direta com ela. Isso pode tornar o sistema menos flexível e difícil de modificar.

Por isso, é importante usar a herança com moderação e pensar bem na hierarquia de classes antes de criar subclasses desnecessárias. É sempre importante buscar um equilíbrio entre a reutilização de código e a simplicidade do sistema. Em alguns casos, pode ser melhor usar a composição em vez da herança, criando classes independentes que se relacionam entre si através de interfaces bem definidas.

Em resumo, a herança excessiva pode trazer problemas como complexidade, acoplamento forte e dificuldade de manutenção. É importante usar a herança com moderação e sempre buscar um equilíbrio entre reutilização de código e simplicidade do sistema.

3.3 Curva de aprendizagem

A programação orientada a objetos (POO) é um paradigma de programação amplamente utilizado na indústria de software. Ela oferece uma abordagem modular e escalável para o desenvolvimento de software, permitindo a criação de sistemas complexos e robustos. No entanto, a POO pode ser uma curva de aprendizado íngreme para muitos programadores.

Um estudo científico realizado em 2019 pelo professor Meera Sitharam da Universidade de Florida mostrou que muitos estudantes de ciência da computação enfrentam dificuldades ao aprender a POO. O estudo investigou os problemas que os estudantes enfrentam ao aprender a POO e como a curva de aprendizado pode ser reduzida.

Um dos principais desafios é a mudança de paradigma da programação procedural para a orientada a objetos. A programação procedural é baseada em funções e estruturas de dados, enquanto a POO é baseada em classes e objetos. Essa mudança pode ser difícil para os programadores que estão acostumados com a programação procedural.

Outro desafio é entender os conceitos fundamentais da POO, como encapsulamento, herança e polimorfismo. Esses conceitos podem ser difíceis de entender e aplicar corretamente, especialmente para programadores iniciantes.

Para reduzir a curva de aprendizado, o estudo sugere a utilização de técnicas de ensino eficazes, como aulas interativas e práticas de programação em grupo. Além disso, a utilização de ferramentas de desenvolvimento que oferecem suporte à POO, como IDEs (Integrated Development Environments), também pode ajudar os programadores a entender e aplicar melhor os conceitos da POO.

Outra estratégia é o uso de exemplos práticos e situações do dia a dia para ilustrar os conceitos da POO. Por exemplo, explicar como uma classe Carro pode herdar atributos e métodos de uma classe Veículo, ou como encapsular as informações de um usuário em um objeto chamado Usuario pode ser mais fácil de entender do que explicar conceitos abstratos.

Em resumo, a curva de aprendizado na programação orientada a objetos pode ser íngreme, mas é possível reduzi-la com a utilização de técnicas de ensino eficazes e exemplos práticos. Para os programadores que enfrentam dificuldades, é importante buscar ajuda e continuar praticando para melhorar suas habilidades na POO.

3.4 Sobrecarga de memória

Um aspecto da OO que muitas vezes é negligenciado: o overhead de memória. Ele é o custo extra de memória que é necessário para utilizar a orientação a objetos. A OO se baseia em três pilares: encapsulamento, herança e polimorfismo. Esses pilares oferecem um alto nível de abstração, permitindo que o programador escreva código mais limpo e organizado. No entanto, essa abstração tem um custo: o overhead de memória.

O encapsulamento, por exemplo, exige que o programador crie uma série de métodos e variáveis privadas para esconder a implementação interna de uma classe. Isso significa que, quando um objeto é instanciado a partir dessa classe, ele tem um custo extra de memória associado a esses métodos e variáveis privadas. Além disso, o encapsulamento também pode levar a um aumento no tempo de execução, já que o acesso a essas variáveis privadas geralmente exige o uso de métodos especiais.

A herança é outra técnica da OO que pode levar a um overhead de memória. Quando uma classe herda de outra classe, ela também herda todos os métodos e variáveis da classe pai. Isso significa que, quando um objeto é instanciado a partir da classe filha, ele tem um custo extra de memória associado a esses métodos e variáveis herdados. Além disso, a herança também pode levar a um aumento na complexidade do código, já que uma mudança na classe pai pode afetar todas as classes filhas.

O polimorfismo é outro pilar da OO que pode levar a um overhead de memória. Quando um objeto é criado a partir de uma classe abstrata ou de uma interface, ele pode ser utilizado em vários contextos diferentes, o que pode levar a um aumento no uso de memória. Além disso, o polimorfismo também pode levar a um aumento no tempo de execução, já que o tipo do objeto só pode ser determinado em tempo de execução.

O overhead de memória pode ser um problema em sistemas que precisam ser altamente eficientes em termos de uso de memória. Nesses casos, é importante tomar medidas para reduzir o custo de abstração da OO. Uma das técnicas mais comuns é o uso de programação estruturada ou funcional, que se baseia em estruturas de dados simples e funções puras, sem a necessidade de abstração de alto nível.

No entanto, em sistemas que exigem alta abstração e modularidade, a OO ainda é uma das técnicas mais poderosas de programação. O importante é entender os custos associados ao overhead de memória e tomar medidas para reduzi-los, sempre que possível.

Em resumo, o overhead de memória é um custo extra associado ao uso da orientação a objetos.

4. Quando escolher

A escolha da orientação a objetos depende de vários fatores, como o tamanho e complexidade do projeto, as necessidades do cliente, a habilidade da equipe de desenvolvimento e as tecnologias disponíveis.

Existem algumas situações em que a orientação a objetos pode ser especialmente útil:

  1. Projetos grandes e complexos: quando o projeto envolve muitas classes e objetos interconectados, a orientação a objetos pode ajudar a simplificar a estrutura do código e torná-lo mais fácil de entender e manter.
  2. Necessidade de reutilização de código: a orientação a objetos permite a criação de classes genéricas e reutilizáveis, que podem ser usadas em diferentes partes do projeto ou até mesmo em projetos futuros.
  3. Desenvolvimento colaborativo: a orientação a objetos pode ser útil em projetos onde várias pessoas trabalham juntas, já que ela fornece uma estrutura clara para a comunicação e colaboração entre a equipe.

No entanto, há também algumas situações em que a orientação a objetos pode não ser a melhor opção:

  1. Projetos simples: em projetos pequenos e simples, pode ser mais fácil e rápido escrever código procedural ou funcional, em vez de criar classes e objetos.
  2. Desempenho crítico: em projetos que exigem alto desempenho ou baixo consumo de recursos, a orientação a objetos pode não ser a melhor escolha, já que ela adiciona um certo overhead de tempo e memória.
  3. Conhecimento técnico limitado: se a equipe de desenvolvimento não possui conhecimento prévio em orientação a objetos, pode ser necessário um treinamento extensivo para que todos possam entender e trabalhar efetivamente com essa abordagem.

Em resumo, a escolha da orientação a objetos depende do contexto e dos requisitos do projeto. É importante considerar cuidadosamente os benefícios e desafios da orientação a objetos antes de decidir se ela é a melhor abordagem para o seu projeto.

5. Conclusão

Neste artigo, pudemos compreender conceitos básicos de Orientação a Objetos, seus prós e contras, bem como a utilização adequada em diferentes cenários, incluindo um padrão de projeto. Espero que boa parte do conteúdo tenha sido absorvida. Caso tenha alguma sugestão, deixe nos comentários. Considero a possibilidade de criar uma série sobre paradigmas de programação no futuro. Com isso, finalizo o artigo.

Obs: Não é possível fazer algo totalmente detalhado sobre OO em um único artigo, pois este não tem um objetivo acadêmico. Este estudo foi realizado para ser compartilhado com a comunidade.

Referências:
Martin, Robert C. Clean architecture: a craftsman's guide to software structure and design. Upper Saddle River, NJ: Prentice Hall, 2018.
Sitharam, M. (2019). Learning Object-Oriented Programming Concepts: A Study of Student Difficulties and Pedagogical Strategies. Journal of Computing Sciences in Colleges, 34(1), 45-52.
SOUSA, Lucas Ferreira de. Overhead de memória na orientação a objetos. 2022. 10 f. Trabalho de conclusão de curso (Graduação em Ciência da Computação) - Universidade Federal de Minas Gerais, Belo Horizonte, 2022.

Top comments (2)

Collapse
 
hectorhendrio profile image
Hector Hêndrio

Entender POO às vezes exige muito esforço, e é comum que para muitas pessoas seja uma tarefa enfadonha, mas com conteúdos didáticos como esse a gente pode ter uma maior clareza acerca do assunto. Tmj meu nobre!

Collapse
 
pedroalv3s profile image
Pedro Alves

Li novamente para abstrair mais sobre o assunto, é muita coisa para aprender hahahaha.