Desbloqueie o poder da Programação Orientada a Objetos e revolucione sua abordagem ao desenvolvimento de software.
- #Java
Dentro da Programação Orientada a Objetos, uma classe é apenas uma descrição do que um objeto deve fazer quando é instanciado e chamado.
Neste artigo tentarei apresentar esse conceito de maneira mais prática, sem animais ou carros.
Vamos considerar a seguinte situação hipotética: estamos encarregados de desenvolver um software para um estacionamento.
Primeiro, o software precisará calcular o valor a ser pago com base no tempo em que o veículo ficou estacionado. Temos o seguinte código:
public class Veiculo {
private String identificador;
private LocalDateTime entrada;
private LocalDateTime saida;
protected float preco = 5.0f; // preço hipotético por hora
public Veiculo(String identificador) {
this.placa = identificador;
this.entrada = LocalDateTime.now();
}
public String getIdentificador() {
return this.identificador;
}
public float calcularPreco() {
this.saida = LocalDateTime.now();
return Math.abs(Duration.between(entrada, saida).toHours()) * preco;
}
}
No código acima, passamos a placa do veículo (placa) no construtor, e a data de entrada é definida automaticamente. A inicialização da data de entrada, por si só, descreve a primeira vantagem da POO que abordarei aqui.
Encapsulamento.
É a arte de ocultar/segurar qualquer informação que precisa ser protegida, completamente controlada pelo código dentro da classe. Neste caso específico, não queremos que os usuários passem a data por conta própria, isso pode ser tratado pelo código. Isso acontece novamente quando o método "calcularPreco" é chamado, pois o preço será calculado quando o método for chamado.
O encapsulamento está intimamente relacionado a outro conceito da POO:
Abstração.
Quando falamos de abstração, estamos falando de manter a complexidade e a lógica dentro da classe, tornando-a indisponível para o usuário. Isso melhora a usabilidade, removendo a complexidade. Assim como qualquer automação, que geralmente é reduzida a um simples botão ou ação.
Nesse caso, o usuário não precisa informar as horas de entrada ou saída, ele só precisa instanciar o veículo e calcular o preço depois de algum tempo.
Claro, em alguns casos, pode ser necessário ter um pouco mais de flexibilidade, mas você pode adicionar credenciais do usuário para controlar esse acesso, por exemplo. Neste artigo, estou descrevendo o conceito puro, em um caso hipotético.
Ok, mas como podemos usar esse código? Bem, vamos instanciar um novo veículo! Você se lembra do único parâmetro necessário para construir o veículo?
public class Main {
public static void main(String[] args) {
Veiculo veiculo = new Veiculo("MEUCARRAO");
// após 3 horas...
float precoTotal = veiculo.calcularPreco();
System.out.println(precoTotal); // 15.0
}
}
Neste método principal, instanciamos a classe "Veiculo" com o identificador "MEUCARRAO". Agora temos um objeto armazenado em uma variável chamada "veiculo". Ao fazer isso, o horário de entrada é automaticamente definido dentro do objeto.
Após algum tempo, o método "calcularPreco()" do nosso objeto é chamado, o qual nos retorna o valor a ser pago. Observe que não precisamos especificar nenhuma data, apenas instanciamos a classe e obtivemos o preço após algum tempo, toda a funcionalidade foi mantida segura dentro do objeto.
Quando alteramos atributos de classe por meio de métodos, estamos encapsulando.
Quando mantemos a lógica dentro da classe, estamos abstraindo.
Ok, tudo bem, podemos calcular o preço de um veículo. Mas agora queremos saber que tipo de veículo está estacionando, e o proprietário pode querer cobrar de forma diferente, com base no tipo do veículo.
Herança
Digamos que o proprietário queira aceitar caminhões no estacionamento. Precisamos escrever uma nova classe com todas as funcionalidades anteriores? Absolutamente não.
Podemos simplesmente criar uma nova classe chamada Caminhao e herdar todas as funcionalidades de Veiculo, estendendo-a:
public class Caminhao extends Veiculo {
public Caminhao(String identificador) {
super(identificador);
super.preco = 10.0f;
}
}
Agora temos uma classe Caminhao, com todos os atributos e métodos de Veiculo, e podemos instanciá-la da mesma maneira que fizemos com Veiculo:
Caminhao caminhao = new Caminhao("MeuCaminhao");
// após 3 horas...
floatprecoTotal = caminhao.calcularPreco();
System.out.println(precoTotal); // 30.0
Você se lembra que na classe Veiculo, o campo "preco" foi descrito como "protected"? Isso significa que apenas as classes no mesmo pacote podem acessá-lo diretamente. Isso impede que ele seja acessado fora do pacote para alterar seu valor, mas nos permite alterar o preço para outros tipos de veículos, uma vez que os veículos estão no mesmo pacote que "Veiculo".
A capacidade de alterar esse valor nos introduz ao próximo tópico.
Polimorfismo
De uma palavra grega que significa "de muitas formas", isso é exatamente o que esperamos dele.
Vamos supor que temos um ArrayList para armazenar os veículos no estacionamento. O ArrayList aceita apenas um tipo de objeto, ou qualquer tipo que se estenda desse tipo.
Sabendo disso, poderíamos simplesmente criar uma List e adicionar qualquer tipo de objeto, desde que sua classe se estenda de Veiculo, como mostrado:
List<Veiculo> estacionamento = new ArrayList<>();
estacionamento.add(new Veiculo("MeuVeiculo"));
estacionamento.add(new Caminhao("MeuCaminhao")); // também é aceito, já que estende da classe Veiculo
Agora o proprietário quer aceitar bicicletas no estacionamento. Mas, para ser justo (ou apenas mudar algo 😁), ele quer cobrar um valor fixo de R$ 7,00 pelo tempo em que a bicicleta ficar estacionada. Isso poderia significar que precisaríamos escrever uma nova classe apenas para a bicicleta, mas não precisamos!
Override
Um dos benefícios do polimorfismo é a capacidade de sobrescrever um método da subclasse. Neste caso, queremos alterar a funcionalidade do método "calcularPreco", como mostrado abaixo:
public class Bicicleta extends Veiculo{
public Bicicleta(String identificador) {
super(identificador);
super.preco = 7.0f;
}
@Override // anotação necessária para indicar que o método está sendo sobrescrito.
public float calcularPreco() {
return this.preco;
}
}
Ao sobrescrevê-lo, podemos dizer que ele deve apenas retornar o preço fixo especificado na classe, mantendo o tipo Veiculo e todas as outras funcionalidades:
Bicicleta bicicleta = new Bicicleta("BMX");
// após n horas...
float precoTotal = bicicleta.calcularPreco();
System.out.println(precoTotal); // 7.0
Overload
Ok, mas e se o caixa estivesse no banheiro e não visse o proprietário do veículo chegando para pegar seu carro. Ele fez o proprietário esperar por 10 minutos e uma nova hora começou. Não é justo cobrar por essa nova hora, foi culpa do caixa.
Eles precisam ser capazes de especificar o horário de saída ao calcular o preço, então temos duas opções:
Escrever um novo método completo, com outro nome e os parâmetros necessários;
Ou podemos apenas sobrecarregar o existente, para que possamos usar o mesmo nome do método ao chamá-lo.
public float calcularPreco() {
this.saida = LocalDateTime.now();
return Math.abs(Duration.between(entrada, saida).toHours()) * price;
}
public float calcularPreco(LocalDateTime saida) {
this.saida = saida;
return Math.abs(Duration.between(entrada, saida).toHours()) * price;
}
Java permite escrever métodos com o mesmo nome e tipo de retorno, mas com parâmetros diferentes, e saber qual deles chamar com base nos parâmetros.
Veiculo veiculo = new Veiculo("MEUCARRAO");
// após 3:05 horas...
float precoTotal = veiculo.calcularPreco(LocalDateTime.now().minusMinutes(10));
System.out.println(precoTotal); // 10.0
Em resumo, a Programação Orientada a Objetos (OOP) oferece benefícios significativos para o desenvolvimento de software:
-Ao encapsular informações dentro de objetos, podemos ocultar detalhes e garantir a segurança do código.
-Com a abstração, mantemos a complexidade lógica dentro da classe, garantindo melhor usabilidade.
-Por meio da herança, podemos reutilizar e estender código existente, economizando tempo e esforço.
-O polimorfismo permite o manuseio flexível de objetos, acomodando diversos tipos em uma interface unificada.
-A sobrescrita permite personalização e adaptabilidade, enquanto a sobrecarga melhora a flexibilidade dos métodos.
-A OOP capacita os desenvolvedores a escrever código organizado, modular e de fácil manutenção.
Suas aplicações práticas promovem a reutilização de código, eficiência e escalabilidade, tornando o desenvolvimento de software mais gerenciável e agradável. Abraçar os princípios da OOP pode aprimorar significativamente suas habilidades de programação e produtividade.
Fico aberto a dúvidas e comentários :)