DEV Community

felipe bs
felipe bs

Posted on

Programação Orientada a Objetos - Java

A essência da POO está em criar modelos/templates de objetos do mundo real dentro do software. Objetos podem ser coisas concretas, como um carro, ou mais abstratas, como uma transação bancária. Cada objeto tem estados e comportamentos. Os estados são as características que definem o objeto, enquanto os comportamentos são as ações que ele pode realizar.

Por exemplo, imagine a classe Carro. Um carro possui atributos que descrevem suas características, como:

  • cor (estado),
  • marca (estado),
  • modelo (estado),
  • quantidade de portas (estado).

Além disso, um carro também pode realizar ações, como:

  1. ligar (comportamento),
  2. andar (comportamento),
  3. parar (comportamento).

Exemplo de classe

Aqui, a classe Carro define o que cada carro (objeto) deve ter e o que ele pode fazer. Podemos criar quantos carros quisermos a partir dessa classe, cada um com seus próprios atributos e comportamentos, mas sempre seguindo a "planta" definida pela classe.

Um exemplo de como seria criar isso no Java seria da seguinte forma:

public class Carro {

    String marca;
    String modelo;
    String cor;
    int quantidade_de_portas;

    // inicializamos a classe Carro sem nenhum valor inicial
    public Carro(Strin) {
    }
}

public static void main(String[] args) {
        Carro carro_exemplo = new Carro("Volkswagen ", "Gol", "prata", 4);
        Carro carro_exemplo2 = new Carro("Fiat", "Uno", "prata", 2);
}
Enter fullscreen mode Exit fullscreen mode

Encapsulamento

O encapsulamento em Java é um dos pilares da Programação Orientada a Objetos (POO) e refere-se ao ato de restringir o acesso direto aos atributos de uma classe, tornando mais claro quais partes de um objeto podem ser acessadas ou modificadas diretamente. Com ele, podemos controlar o uso e a manipulação dos dados, garantindo maior segurança e integridade.

Em Java, o encapsulamento é implementado através de modificadores de acesso, como privatee public, e com o uso de métodos especiais, conhecidos como getters e setters, que permitem acessar ou modificar os atributos de uma classe de forma controlada.

No exemplo abaixo:

Encapsulamento exemplo

A classe ContaBancaria contém dois atributos: titular e saldo. Como ambos estão declarados com o modificador public, eles podem ser acessados e modificados diretamente de qualquer parte do sistema, sem qualquer controle ou validação. Isso pode ser perigoso, pois dados sensíveis podem ser alterados em momentos inadequados ou de forma incorreta.

Agora, com o uso de encapsulamento, podemos proteger esses dados:

public class ContaBancaria {
    private String titular;
    private double saldo;

    // Getter
    public String getTitular() {
        return titular;
    }

    public double getSaldo() {
        return saldo;
    }

    // Setter
    public void setTitular(String titular) {
        this.titular = titular;
    }

    public void setSaldo(double saldo) {
        this.saldo = saldo;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, os atributos titular e saldo foram modificados para private, o que significa que não podem ser acessados diretamente de fora da classe ContaBancaria. Em vez disso, criamos métodos get e set para cada um desses atributos, que permitem o acesso controlado. Isso evita, por exemplo, que o saldo seja alterado diretamente por qualquer parte do sistema sem passar por regras de negócio ou validações.

É importante ressaltar que o getter é utilizado sempre que precisamos controlar como os dados são fornecidos a quem acessa um valor dessa classe de fora. Da mesma forma, o setter é criado para implementar mudanças de forma mais segura e restrita nos atributos, permitindo a aplicação de regras ou validações antes de alterar os valores.

Agora, para acessar ou modificar os atributos da ContaBancaria, o código deve chamar os métodos adequados:

ContaBancaria conta = new ContaBancaria();
conta.setTitular("Felipe");
conta.setSaldo(0.0);

System.out.println(conta.getTitular());

Enter fullscreen mode Exit fullscreen mode

Com isso, garantimos que a única forma de alterar o titular ou o saldo é através dos métodos específicos, o que protege o sistema contra mudanças indesejadas ou erros, como:

conta.titular = "Bruno";
Enter fullscreen mode Exit fullscreen mode

Herança

Herança é um dos pilares fundamentais da Programação Orientada a Objetos (POO). O conceito de herança permite que uma classe (chamada de subclasse) herde características e comportamentos de outra classe (chamada de superclasse). Com a herança, é possível reutilizar e especializar código existente, o que torna o desenvolvimento de sistemas mais eficiente e organizado.

Como a herança é útil?

Imagine que você tem uma classe chamada Cartao, que possui atributos e comportamentos comuns a diferentes tipos de cartões, como cartões de débito e crédito. Em vez de repetir esses atributos e comportamentos em cada tipo de cartão, você pode centralizá-los na classe Cartao e, em seguida, herdar essas características nas subclasses CartaoDebito e CartaoCredito.

Exemplo abaixo:

Herança exemplo

No diagrama acima, a classe Cartao possui três atributos comuns: codigo_cartao, numero_cartao e digito_cartao. Esses atributos são marcados como protected, o que significa que eles podem ser acessados e modificados diretamente pelas subclasses, mas não por outras classes externas.

Agora, temos duas subclasses:

  • CartaoDebito: herda os atributos da classe Cartao e adiciona comportamentos e atributos específicos, como data_pagamento_deposito e data_saque, além de métodos para sacar dinheiro e depositar dinheiro.
  • CartaoCredito: também herda os atributos de Cartao, mas adiciona características específicas, como se_pode_aproximacao e comportamentos para pagar com/sem aproximação e verificar o limite do cartão.

Nesse exemplo, as classes CartaoDebito e CartaoCredito herdam atributos e métodos da classe Cartao. Dessa forma, reutilizamos o código base da superclasse, o que simplifica a manutenção e o desenvolvimento. Além disso, cada subclasse pode implementar suas próprias versões de métodos ou adicionar novos comportamentos específicos, garantindo a flexibilidade e a especialização necessárias para cada tipo de cartão.

Exemplo abaixo:

public abstract class Cartao {
    protected String codigo_cartao;
    protected String numero_cartao;
    protected String digito_cartao;

    // Métodos comuns
    public abstract void sacarDinheiro(double valor);
    public abstract void depositarDinheiro(double valor);
}

// Subclasse Cartão de Débito
public class CartaoDebito extends Cartao {
    private String data_deposito;
    private String data_saque;

    @Override
    public void sacarDinheiro(double valor) {
        System.out.println("Sacando dinheiro do Cartão de Débito");
        // Lógica específica para o saque
        // Adicionar data de saque;
    }

    @Override
    public void depositarDinheiro(double valor) {
        System.out.println("Depositando dinheiro no Cartão de Débito");
        // Lógica específica para o depósito
        // Adicionar data de deposito;
    }
}

// Subclasse Cartão de Crédito
public class CartaoCredito extends Cartao {
    private String data_pagamento;
    private boolean se_pode_aproximacao;

    @Override
    public void sacarDinheiro(double valor) {
        System.out.println("Sacando dinheiro do Cartão de Crédito");
        // Lógica específica para o saque
    }

    @Override
    public void depositarDinheiro(double valor) {
        System.out.println("Depositando dinheiro no Cartão de Crédito");
        // Lógica específica para o depósito
    }

    public void pagar() {
        if (se_pode_aproximacao) {
            System.out.println("Pagamento com aproximação");
        } else {
            System.out.println("Pagamento sem aproximação");
        }
        // Adiciona data de pagamento;
    }

    public void verificarSeTemLimite() {
        // Lógica para verificar o limite do cartão
        System.out.println("Verificando limite do Cartão de Crédito");
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, as classes CartaoDebito e CartaoCredito herdam atributos e métodos da classe Cartao. Dessa forma, reutilizamos o código base da superclasse, o que simplifica a manutenção e o desenvolvimento. Além disso, cada subclasse pode implementar suas próprias versões de métodos ou adicionar novos comportamentos específicos, garantindo a flexibilidade e a especialização necessárias para cada tipo de cartão.

Polimorfismo

Agora que entendemos a herança, vamos falar algo que esta muito ligado a ele, o polimorfismo em termos simples significa “muitas formas” — ou seja, a capacidade de um objeto de assumir diferentes formas. No contexto da POO, o polimorfismo permite que uma mesma ação (como usar um procedimento) se comporte de maneiras diferentes, dependendo do objeto que a executa.

Isso é extremamente útil porque permite que tratemos diferentes objetos de forma uniforme, mas ainda permitindo que cada objeto tenha seu próprio comportamento específico.

No exemplo que vimos anteriormente, temos uma classe base chamada Cartao, que é estendida por duas classes: CartaoDebito e CartaoCredito. Cada uma dessas subclasses implementa comportamentos específicos para o tipo de cartão, como sacarDinheiro() e depositarDinheiro().

Agora, vamos ver como o polimorfismo pode ser utilizado para tratar diferentes tipos de cartões de maneira uniforme, mas permitindo que cada cartão aja de acordo com suas características.

public class Main {
    public static void processarSaque(Cartao cartao, double valor) {
        cartao.sacarDinheiro(valor);
    }

    public static void processarDeposito(Cartao cartao, double valor) {
        cartao.depositarDinheiro(valor);
    }

    public static void main(String[] args) {
        Cartao cartaoDebito = new CartaoDebito();
        Cartao cartaoCredito = new CartaoCredito();

        // Polimorfismo em ação: o método sacarDinheiro se comporta de maneira diferente para cada tipo de cartão
        processarSaque(cartaoDebito, 100);  // Comportamento específico do Cartão de Débito
        processarSaque(cartaoCredito, 200); // Comportamento específico do Cartão de Crédito

        processarDeposito(cartaoDebito, 500);  // Depósito no Cartão de Débito
        processarDeposito(cartaoCredito, 1000); // Depósito no Cartão de Crédito
    }
}
Enter fullscreen mode Exit fullscreen mode

No código acima, temos dois métodos chamados processarSaque e processarDeposito. Esses métodos recebem um objeto do tipo Cartao como argumento, o que significa que eles podem aceitar qualquer subclasse de Cartao, como CartaoDebito ou CartaoCredito.

O comportamento polimórfico acontece quando chamamos o método sacarDinheiro() e depositarDinheiro() em diferentes tipos de cartões. Mesmo que os métodos sejam chamados da mesma forma, o comportamento muda com base no tipo de objeto (se é um CartaoDebito ou um CartaoCredito). Isso é o polimorfismo em ação — o método se comporta de forma diferente dependendo do objeto que o invoca.

O polimorfismo torna o código mais flexível e escalável. Veja que no exemplo, os métodos processarSaque e processarDeposito não precisaram ser alterados ou duplicados para lidar com diferentes tipos de cartão. Eles aceitam qualquer objeto que seja uma instância da classe Cartao, e o comportamento apropriado é decidido dinamicamente, com base no tipo específico do cartão.

Interface

A interface, por último, mas não menos importante, é um dos pilares da Programação Orientada a Objetos (POO) e é amplamente utilizada. Ela tem o efeito oposto da herança: enquanto a herança permite que uma classe filha herde características da classe pai, a interface obriga todas as classes que a implementam a fornecer suas próprias lógicas para os métodos definidos. Isso garante que cada classe atenda a um contrato específico, promovendo consistência e reutilização de código.

Exemplo abaixo:

Interface exemplo

Nesse exemplo, temos a interface chamada Notificacao, que possui um método chamado enviarMensagem(mensagem). Temos duas classes que a implementam: NotificacaoSMS e NotificacaoEmail. Embora a interface seja semelhante à herança, onde estendemos uma classe abstrata, ela se concentra em definir apenas métodos/comportamentos, evitando a criação de regras de negócio. Isso obriga todas as classes que a implementam a fornecer suas próprias implementações desses métodos, permitindo que cada uma crie suas regras de negócio específicas.

Exemplo abaixo:

public interface Notificacao {
    void enviarMensagem(String mensagem);
}

public class NotificacaoEmail implements Notificacao {

    // compormanento "forçado" pela interface
    @Override
    public void enviarMensagem(String mensagem) {
        System.out.println("E-mail: " + mensagem);
    }
}


public class NotificacaoSMS implements Notificacao {

    // compormanento "forçado" pela interface
    @Override
    public void enviarMensagem(String mensagem) {
        System.out.println("SMS: " + mensagem);
    }
}


public class Main {
    public static void main(String[] args) {
        Notificacao notificacaoEmail = new NotificacaoEmail();
        Notificacao notificacaoSMS = new NotificacaoSMS();

        notificacaoEmail.enviarMensagem("Olá, esta é uma notificação via e-mail!");
        notificacaoSMS.enviarMensagem("Olá, esta é uma notificação via SMS");
    }
}
Enter fullscreen mode Exit fullscreen mode

Como observado no exemplo acima, ambas as classes precisam enviar notificações ao usuário. Podemos obrigar que todas implementem essa funcionalidade, evitando a duplicação de código e promovendo a reutilização de componentes já criados. Na parte do Main, conseguimos ver as duas classes enviando mensagens de acordo com seus métodos de envio específicos.

Conclusão

A Programação Orientada a Objetos é uma abordagem poderosa para organizar seu código de forma clara e eficiente. Com conceitos como encapsulamento, herança e polimorfismo, agora quando você ouvir falar que uma linguagem é desenvolvida em cima do POO ou tem suporte, já consegue imaginar quais tipos de recursos ela tem a oferece.

Links auxiliares

Explicação sobre Abstract
Explicação sobre Interface

Top comments (0)