A Programação Orientada a Objetos (OOP) é um paradigma de programação que organiza o design do software em torno de "objetos". Objetos são instâncias de classes. O foco aqui é nos objetos que os desenvolvedores desejam manipular e a interação entre eles. A Orientação a Objetos oferece uma estrutura que facilita a modularidade, reusabilidade e escalabilidade de um sistema. Quando o código é modular, isso significa que alterações em uma parte do sistema têm menor probabilidade de afetar outras partes, simplificando a manutenção. Já a reusabilidade se manifesta na capacidade de usar classes existentes para construir novas funcionalidades, seja criando múltiplos objetos a partir de uma mesma classe, utilizando objetos de uma classe dentro de outra (composição), ou herdando características de uma classe pai. Vamos falar mais sobre isso adiante.
Por que OOP com Java?
Java, desde sua criação, foi pensado para ser uma linguagem orientada a objetos. Seus recursos suportam nativamente os conceitos de herança, polimorfismo, encapsulamento e abstração (conceitos que serão mais explorados em breve). Então, se você deseja ou está aprendendo Java, a Orientação a Objetos não é apenas uma opção, mas um aspecto fundamental e intrínseco ao desenvolvimento eficaz e idiomático em Java.
Classes e Objetos
Definição de Classes
No cerne da OOP estão as classes. As classes podem ser compreendidas como um projeto, protótipo ou modelo a ser seguido e a partir do qual os objetos são criados. As classes definem um conjunto de atributos (também conhecidos como campos ou variáveis de instância) que representam o estado do objeto e um conjunto de métodos que definem o comportamento de um objeto. As classes ainda têm algumas características como:
- Modificadores Definem características como a visibilidade e a capacidade de herança/instanciação. Exemplos para visibilidade de classes de nível superior são public (acessível de qualquer lugar) e o acesso padrão (package-private, acessível apenas dentro do mesmo pacote). Outros modificadores importantes incluem abstract (a classe não pode ser instanciada) e final (a classe não pode ser herdada).
Nome da Classe: Por convenção, sempre iniciando com letra maiúscula.
Superclasse (opcional): A classe da qual a classe atual herda, indicada pela palavra-chave extends.
Interfaces (Opcional): Interfaces que a classe implementa, indicado pela palavra-chave implements.
Corpo da Classe: Delimitado por chaves {}
, contém a declaração dos campos, construtores e métodos.
Definição de Objetos
Os objetos são as instâncias concretas de uma classe. Se uma classe é o projeto de uma casa, o objeto é a casa construída a partir desse projeto. Os objetos em Java possuem as seguintes características principais:
- Estado: Representado pelos valores de seus atributos em determinado momento.
- Comportamento: Definido pelos métodos que podem ser invocados nesse objeto.
- Identidade: Uma propriedade única que distingue um objeto de outro, mesmo que tenham o mesmo estado. Em Java, essa identidade é tipicamente o endereço de memória do objeto. Os objetos são criados com a palavra-chave new.
// Exemplo de definição de classe e criação de objeto
class Caneta {
// Atributos
String modelo;
String cor;
float ponta;
int carga;
boolean tampada;
// Métodos
void status() {
System.out.println("Modelo: " + this.modelo);
System.out.println("Uma caneta " + this.cor);
System.out.println("Ponta: " + this.ponta);
System.out.println("Carga: " + this.carga);
System.out.println("Está tampada? " + this.tampada);
}
void rabiscar() {
if (this.tampada) {
System.out.println("ERRO! Não posso rabiscar com a caneta tampada.");
} else {
System.out.println("Estou rabiscando...");
}
}
void tampar() {
this.tampada = true;
}
void destampar() {
this.tampada = false;
}
}
public class AulaObjetos {
public static void main(String[] args) {
Caneta c1 = new Caneta(); // Criação de um objeto Caneta
c1.modelo = "BIC Cristal";
c1.cor = "Azul";
// c1.ponta = 0.5f; // Se o atributo fosse público
c1.carga = 80;
c1.tampada = true;
c1.status();
c1.rabiscar();
System.out.println("\n--- Outra Caneta ---");
Caneta c2 = new Caneta();
c2.modelo = "Faber-Castell";
c2.cor = "Vermelha";
c2.destampar();
c2.status();
c2.rabiscar();
}
}
Os objetos são inicializados a partir de seus construtores, que são semelhantes a um método, porém são invocados durante a criação de um objeto. A sua principal funcionalidade é inicializar o estado do novo objeto, ou seja, passar valores iniciais para seus atributos. Os construtores possuem o mesmo nome da classe e não têm nenhum tipo de retorno (nem mesmo void).
// Exemplo de classe com construtor parametrizado
class Carro {
String marca;
String modelo;
int ano;
String cor;
// Construtor parametrizado
public Carro(String marca, String modelo, int ano, String cor) {
this.marca = marca;
this.modelo = modelo;
this.ano = ano;
this.cor = cor;
}
void exibirInformacoes() {
System.out.println("Marca: " + this.marca);
System.out.println("Modelo: " + this.modelo);
System.out.println("Ano: " + this.ano);
System.out.println("Cor: " + this.cor);
}
}
public class TesteCarro {
public static void main(String[] args) {
// Criando um objeto Carro usando o construtor parametrizado
Carro meuCarro = new Carro("Fiat", "Uno", 2020, "Prata");
meuCarro.exibirInformacoes();
System.out.println("\n--- Outro Carro ---");
Carro outroCarro = new Carro("Volkswagen", "Gol", 2022, "Branco");
outroCarro.exibirInformacoes();
}
}
Existem duas palavras-chave importantes relacionadas ao contexto de objetos e herança: this
e super
.
- This: Refere-se à instância atual do objeto dentro do qual o código está sendo executado. Seu principal papel é distinguir variáveis de instância de parâmetros de métodos ou construtores com o mesmo nome. Sua segunda função é invocar outro construtor na mesma classe e sua terceira função é passar a instância atual como argumento em uma chamada de método ou construtor.
// Exemplo do uso da palavra-chave 'this'
class Retangulo {
private double largura;
private double altura;
// Construtor 1: usa 'this' para diferenciar parâmetro de atributo
public Retangulo(double largura, double altura) {
this.largura = largura; // 'this.largura' é o atributo, 'largura' é o parâmetro
this.altura = altura;
}
// Construtor 2: chama outro construtor da mesma classe usando 'this()'
public Retangulo(double lado) {
this(lado, lado); // Chama o construtor Retangulo(double largura, double altura)
}
public double calcularArea() {
return this.largura * this.altura;
}
public void imprimirDimensoes() {
System.out.println("Dimensões: " + this.largura + "x" + this.altura);
}
// Método que passa a instância atual como argumento
public void compararComOutroRetangulo(Retangulo outro) {
if (this.calcularArea() > outro.calcularArea()) {
System.out.println("Este retângulo é maior.");
} else if (this.calcularArea() < outro.calcularArea()) {
System.out.println("O outro retângulo é maior.");
} else {
System.out.println("Os retângulos têm a mesma área.");
}
}
}
public class TesteThis {
public static void main(String[] args) {
Retangulo r1 = new Retangulo(10, 5); // Usa o construtor 1
r1.imprimirDimensoes();
System.out.println("Área r1: " + r1.calcularArea());
Retangulo r2 = new Retangulo(7); // Usa o construtor 2 (quadrado)
r2.imprimirDimensoes();
System.out.println("Área r2: " + r2.calcularArea());
r1.compararComOutroRetangulo(r2);
}
}
- Super: Essa palavra-chave é usada para referenciar membros (campos e métodos) da superclasse imediata e para invocar construtores da superclasse.
// Exemplo do uso da palavra-chave 'super'
class Animal {
String nome;
String som;
public Animal(String nome, String som) {
this.nome = nome;
this.som = som;
}
public void fazerSom() {
System.out.println(this.nome + " faz: " + this.som);
}
public void comer() {
System.out.println(this.nome + " está comendo.");
}
}
class Cachorro extends Animal {
String raca;
public Cachorro(String nome, String raca) {
super(nome, "Au Au"); // Chama o construtor da superclasse (Animal)
this.raca = raca;
}
// Sobrescrevendo o método da superclasse
@Override
public void fazerSom() {
System.out.println("O cachorro " + this.nome + " (" + this.raca + ") late: " + this.som);
}
public void buscarOsso() {
System.out.println(this.nome + " foi buscar o osso!");
}
public void exibirInfo() {
super.fazerSom(); // Chama o método fazerSom() da superclasse Animal
this.buscarOsso(); // Chama o método da própria classe Cachorro
super.comer(); // Chama o método comer() da superclasse Animal
}
}
public class TesteSuper {
public static void main(String[] args) {
Cachorro meuCachorro = new Cachorro("Rex", "Labrador");
meuCachorro.fazerSom(); // Chama o método sobrescrito em Cachorro
meuCachorro.comer(); // Chama o método herdado de Animal
meuCachorro.buscarOsso();
System.out.println("\n--- Exibindo Info Completa ---");
meuCachorro.exibirInfo();
}
}
O uso correto de this()
e super()
é fundamental para o encadeamento e a inicialização em heranças. A chamada a um construtor da superclasse (usando super()) é crucial. Se a superclasse não possuir um construtor padrão (sem argumentos) e o construtor da subclasse não invocar explicitamente um construtor da superclasse com super(...), ocorrerá um erro de compilação. A omissão ou uso incorreto pode levar a objetos não completamente inicializados. A regra de que this() ou super() deve ser a primeira instrução assegura uma ordem lógica de inicialização, impedindo o uso simultâneo e forçando uma cadeia linear.
Os 4 Pilares da OOP:
A Programação Orientada a Objetos está apoiada em quatro conceitos fundamentais, também conhecidos como pilares: Encapsulamento, Herança, Polimorfismo e Abstração.
Encapsulamento
O encapsulamento é o mecanismo que permite agrupar dados (atributos) e os métodos que operam esses dados em uma mesma unidade, a classe. Além disso, o encapsulamento restringe o acesso direto ao estado interno do objeto, protegendo-o de modificações externas indesejadas. No Java, isso é alcançado por meio dos modificadores de acesso. Um encapsulamento bem aplicado pode trazer uma série de benefícios, como:
- Proteção de dados: O estado interno do objeto é protegido contra acesso e modificações não autorizadas.
Controle de acesso: A classe controla como seus dados são modificados e acessados. Geralmente via métodos getters e setters.
Modularidade: Objetos encapsulados são como "caixas-pretas", onde a implementação interna pode ser alterada sem afetar outras partes do sistema que dependem da interface pública do objeto.
Flexibilidade e Manutenibilidade: Mudanças na representação interna dos dados ou na lógica dos métodos podem ser feitas sem quebrar o código cliente, desde que a interface pública permaneça consistente.
Em Java,os modificadores de acesso controlam a visibilidade de classes, campos, métodos e construtores.
- public: oferece acesso irrestrito;
- protected: Permite acesso dentro da classe, no mesmo pacote e em subclasses;
- Padrão (package-private): Restringe o acesso ao mesmo pacote;
- private: limita o acesso apenas à classe declarada, sendo o nível mais restritivo para encapsulamento.(Exemplo de encapsulamento já foi implicitamente mostrado na classe Caneta com atributos private e métodos public para interagir com eles, ou na classe Retangulo com atributos private e construtores/métodos public)
Herança
A herança é um mecanismo fundamental na OOP. Ela permite que uma classe (chamada de subclasse ou classe filha) herde métodos e atributos de outra classe (chamada de superclasse ou classe pai). Em Java, uma herança pode ser implementada usando a palavra-chave extends. A herança cria uma relação de "É-UM" entre uma superclasse e uma subclasse. Por exemplo, se uma classe Carro
herda de Veiculo
, então falamos que Carro é um Veiculo. Isso significa que além da classe Carro adquirir todos os atributos de Veiculo, ela ainda pode ter seus próprios métodos e atributos, ou modificar (sobrescrever) o comportamento dos métodos herdados.
A herança em Java possibilita a criação de novas classes (subclasses) baseadas em classes existentes (superclasses), promovendo reutilização de código, organização hierárquica e evitando duplicação. As subclasses podem estender ou modificar o comportamento da superclasse, facilitando a manutenção e resultando em um design modular e extensível.
Além disso, a herança contribui para o polimorfismo, permitindo tratar objetos de diferentes subclasses de maneira uniforme.(O exemplo da classe Cachorro estendendo Animal já ilustra a herança).
Polimorfismo
O Polimorfismo, que significa “muitas formas”, é a capacidade do objeto de assumir diferentes formas. Ou, se você quiser uma definição mais técnica: a capacidade de uma referência de variável de um tipo de superclasse referir-se a objetos de suas subclasses. Ele permite que diferentes tipos de objetos respondam à mesma chamada de método de maneiras diferentes, específicas de seu tipo. Existem ainda dois tipos de polimorfismo:
- Polimorfismo de Sobrescrita (Overriding ou Polimorfismo em Tempo de Execução): Este tipo de polimorfismo acontece quando uma subclasse implementa de maneira específica um método já existente em sua superclasse (sobrescrita de método). A determinação de qual versão do método será executada ocorre durante o tempo de execução do programa, fundamentada no tipo concreto do objeto referenciado.
// Exemplo de Polimorfismo de Sobrescrita
class FormaGeometrica {
public void desenhar() {
System.out.println("Desenhando uma forma geométrica genérica.");
}
public double calcularArea() {
System.out.println("Não é possível calcular a área de uma forma genérica.");
return 0;
}
}
class Circulo extends FormaGeometrica {
private double raio;
public Circulo(double raio) {
this.raio = raio;
}
@Override // Anotação que indica sobrescrita de método
public void desenhar() {
System.out.println("Desenhando um círculo de raio " + this.raio);
}
@Override
public double calcularArea() {
return Math.PI * this.raio * this.raio;
}
}
class Quadrado extends FormaGeometrica {
private double lado;
public Quadrado(double lado) {
this.lado = lado;
}
@Override
public void desenhar() {
System.out.println("Desenhando um quadrado de lado " + this.lado);
}
@Override
public double calcularArea() {
return this.lado * this.lado;
}
}
public class TestePolimorfismoSobrescrita {
public static void main(String[] args) {
FormaGeometrica forma1 = new Circulo(5.0); // Objeto Circulo referenciado por FormaGeometrica
FormaGeometrica forma2 = new Quadrado(4.0); // Objeto Quadrado referenciado por FormaGeometrica
FormaGeometrica forma3 = new FormaGeometrica();
forma1.desenhar(); // Chama o método desenhar() de Circulo
System.out.println("Área do círculo: " + forma1.calcularArea());
forma2.desenhar(); // Chama o método desenhar() de Quadrado
System.out.println("Área do quadrado: " + forma2.calcularArea());
forma3.desenhar(); // Chama o método desenhar() de FormaGeometrica
forma3.calcularArea();
}
}
- Polimorfismo de Sobrecarga (Overloading ou Polimorfismo em Tempo de Compilação): Este tipo de polimorfismo acontece quando uma mesma classe possui diversos métodos com o mesmo nome, porém com listas de parâmetros distintas (seja no número, no tipo dos argumentos, ou em ambos). O compilador Java é responsável por determinar qual método deve ser executado durante a compilação, considerando os argumentos presentes na chamada do método. Um exemplo comum desse tipo de polimorfismo é a sobrecarga de construtores, já mencionada. Um caso ilustrativo é uma classe Calculadora, que pode conter várias versões do método somar() com diferentes parâmetros.
// Exemplo de Polimorfismo de Sobrecarga (Overloading)
class Calculadora {
// Método somar com dois inteiros
public int somar(int a, int b) {
System.out.println("Somando dois inteiros");
return a + b;
}
// Método somar com três inteiros (mesmo nome, lista de parâmetros diferente)
public int somar(int a, int b, int c) {
System.out.println("Somando três inteiros");
return a + b + c;
}
// Método somar com dois doubles (mesmo nome, tipo de parâmetros diferente)
public double somar(double a, double b) {
System.out.println("Somando dois doubles");
return a + b;
}
// Método somar com um inteiro e um double
public double somar(int a, double b) {
System.out.println("Somando um inteiro e um double");
return a + b;
}
}
public class TestePolimorfismoSobrecarga {
public static void main(String[] args) {
Calculadora calc = new Calculadora();
System.out.println("Soma 1: " + calc.somar(10, 20));
System.out.println("Soma 2: " + calc.somar(10, 20, 30));
System.out.println("Soma 3: " + calc.somar(3.5, 2.5));
System.out.println("Soma 4: " + calc.somar(5, 7.5));
}
}
Abstração
Abstração, um dos pilares da Programação Orientada a Objetos (OOP), consiste em identificar as características essenciais de um objeto, negligenciando detalhes de implementação que não são relevantes ou que são complexos. Em vez de se concentrar em "como" um objeto realiza suas funções, a abstração prioriza "o quê" ele faz.
O objetivo principal é simplificar a interação com objetos complexos, expondo apenas uma interface pertinente e ocultando a complexidade interna. Em Java, a abstração é principalmente alcançada por meio de:
Classes Abstratas: Estas são classes que não podem ser diretamente instanciadas e podem conter tanto métodos abstratos (sem implementação, a serem definidos por subclasses concretas) quanto métodos concretos.
Interfaces: Elas definem um contrato de comportamento que as classes podem implementar. Uma interface especifica um conjunto de métodos que qualquer classe que a implemente deve fornecer.
A abstração contribui para a redução do impacto de alterações na implementação interna de um objeto, desde que sua interface pública (a abstração em si) permaneça inalterada. Isso fomenta um baixo acoplamento entre os diversos componentes de um sistema.
É importante compreender que os quatro pilares da OOP – encapsulamento, herança, polimorfismo e abstração – estão interligados e são fundamentais para um design orientado a objetos eficaz. O encapsulamento, por exemplo, é crucial para uma abstração efetiva. Ao ocultar os detalhes internos e a complexidade de um objeto (restringindo o acesso direto ao seu estado e expondo apenas operações bem definidas), torna-se viável apresentar uma interface simplificada – a abstração – para o exterior. Sem o encapsulamento dos detalhes internos, seria inviável criar uma abstração confiável e estável, pois os usuários do objeto dependeriam de detalhes que poderiam ser modificados a qualquer momento. Assim, o encapsulamento protege o "como" para que a abstração possa apresentar de maneira consistente o "o quê".
Similarmente, a herança desempenha um papel fundamental para alcançar o polimorfismo de sobrescrita. A herança permite que uma subclasse herde métodos de uma superclasse. A sobrescrita de métodos, por sua vez, possibilita que a subclasse forneça uma implementação específica para um método herdado. O polimorfismo de sobrescrita (ou dinâmico) significa que, ao invocar um método em um objeto, a versão do método que é executada é aquela definida pela classe real do objeto, e não pelo tipo da variável de referência. Sem a herança, não haveria um método da superclasse para ser sobrescrito pela subclasse. Portanto, a herança oferece a estrutura (os métodos herdados) e a sobrescrita fornece a especialização, ambos essenciais para o funcionamento do polimorfismo dinâmico.No entanto, é crucial notar que o uso inadequado ou excessivo da herança pode gerar problemas significativos. Hierarquias de classes excessivamente profundas ou mal estruturadas podem se tornar inflexíveis e de difícil manutenção.
Um problema bem conhecido é o "problema da base frágil", onde modificações na superclasse, mesmo que aparentemente seguras, podem inadvertidamente quebrar o comportamento das subclasses de maneiras inesperadas. Isso ocorre porque a herança estabelece um forte acoplamento entre a superclasse e suas subclasses; a subclasse herda não apenas a interface, mas frequentemente também a implementação da superclasse. Se a implementação da superclasse for alterada, ou se novos métodos forem adicionados que entrem em conflito com os da subclasse, a integridade do sistema pode ser comprometida. Essa observação sobre os riscos da herança sugere que, embora seja um recurso poderoso, deve ser utilizada com discernimento. Alternativas como a composição, que modela um relacionamento "tem-um" em vez de "é-um", frequentemente oferecem maior flexibilidade e robustez, promovendo um acoplamento mais fraco.
A decisão entre herança e composição é uma escolha de design crítica que impacta diretamente a manutenibilidade e a escalabilidade do sistema, um tópico a ser explorado em mais detalhes posteriormente.
Aprofundamento em Abstrações
A abstração é um dos pilares da OOP, permitindo que se lide com a complexidade ao modelar apenas os aspectos relevantes de um objeto ou sistema para um determinado contexto.
Em Java, classes abstratas e interfaces são os principais mecanismos para alcançar a abstração.
-
Classes Abstratas: Em Java, uma classe abstrata, declarada com a palavra-chave
abstract
, não pode ser instanciada diretamente. Seu propósito principal é servir como superclasse para outras classes, fornecendo uma estrutura comum e, opcionalmente, implementações compartilhadas.- Características das classes abstratas: Podem conter métodos abstratos (declarados com abstract e sem implementação), que devem ser implementados por suas subclasses concretas.Podem incluir métodos concretos (com implementação), que são herdados ou podem ser sobrescritos pelas subclasses. Podem possuir construtores, que são chamados durante a criação de instâncias de subclasses concretas para inicializar os campos definidos na classe abstrata. Podem declarar variáveis de instância e estáticas, assim como classes concretas. Classes abstratas são úteis para estabelecer uma base para classes relacionadas, permitindo o reaproveitamento de código e a definição de um contrato parcial a ser completado pelas subclasses. Um exemplo comum é uma classe abstrata Veiculo com um método abstrato acelerar() e um método concreto iniciarMotor().
// Exemplo de Classe Abstrata
abstract class Funcionario {
protected String nome;
protected double salarioBase;
public Funcionario(String nome, double salarioBase) {
this.nome = nome;
this.salarioBase = salarioBase;
}
// Método abstrato - deve ser implementado pelas subclasses
public abstract double calcularSalario();
// Método concreto - pode ser usado ou sobrescrito pelas subclasses
public void exibirDetalhes() {
System.out.println("Nome: " + this.nome);
System.out.println("Salário Base: R$" + this.salarioBase);
}
public String getNome() {
return nome;
}
}
class Gerente extends Funcionario {
private double bonus;
public Gerente(String nome, double salarioBase, double bonus) {
super(nome, salarioBase); // Chama construtor da classe abstrata
this.bonus = bonus;
}
@Override
public double calcularSalario() {
return this.salarioBase + this.bonus;
}
@Override
public void exibirDetalhes() {
super.exibirDetalhes(); // Chama o método da superclasse
System.out.println("Bônus: R$" + this.bonus);
System.out.println("Salário Total (Gerente): R$" + this.calcularSalario());
}
}
class Programador extends Funcionario {
private int horasExtras;
private double valorHoraExtra;
public Programador(String nome, double salarioBase, int horasExtras, double valorHoraExtra) {
super(nome, salarioBase);
this.horasExtras = horasExtras;
this.valorHoraExtra = valorHoraExtra;
}
@Override
public double calcularSalario() {
return this.salarioBase + (this.horasExtras * this.valorHoraExtra);
}
@Override
public void exibirDetalhes() {
super.exibirDetalhes();
System.out.println("Horas Extras: " + this.horasExtras);
System.out.println("Valor Hora Extra: R$" + this.valorHoraExtra);
System.out.println("Salário Total (Programador): R$" + this.calcularSalario());
}
}
public class TesteClasseAbstrata {
public static void main(String[] args) {
// Funcionario f = new Funcionario("Teste", 1000); // Erro! Não pode instanciar classe abstrata
Gerente g = new Gerente("Carlos Silva", 5000.00, 1500.00);
Programador p = new Programador("Ana Paula", 3500.00, 20, 50.00);
g.exibirDetalhes();
System.out.println("---");
p.exibirDetalhes();
}
}
- Interfaces: Uma interface em Java é um tipo de referência totalmente abstrato que define um contrato de comportamento que as classes se comprometem a implementar. Ela especifica o que uma classe deve fazer, e não como (com exceção de métodos default e static introduzidos no Java 8).
Interfaces são declaradas com a palavra-chave interface.
Características das interfaces: Antes do Java 8, continham apenas assinaturas de métodos abstratos (implicitamente public abstract) e constantes (campos implicitamente public static final). A partir do Java 8, podem incluir métodos default (com implementação padrão que as classes implementadoras podem usar ou sobrescrever) e métodos static (métodos utilitários associados à interface, não às suas instâncias).
Não podem ser instanciadas diretamente. Uma classe pode implementar múltiplas interfaces (usando implements), possibilitando uma forma de herança múltipla de tipo.São ideais para definir contratos implementados por classes sem relação hierárquica. Exemplos incluem as interfaces Comparable e Runnable.
// Exemplo de Interface
interface Autenticavel {
// Atributo (constante) implicitamente public static final
int SENHA_PADRAO = 12345;
// Método abstrato implicitamente public abstract
boolean autenticar(int senha);
// Método default (Java 8+)
default void logAcesso() {
System.out.println("Acesso registrado para o autenticável.");
}
// Método static (Java 8+)
static void exibirRegrasSenha() {
System.out.println("A senha deve ter no mínimo 4 dígitos.");
}
}
class Usuario implements Autenticavel {
private String login;
private int senha;
public Usuario(String login, int senha) {
this.login = login;
this.senha = senha;
}
@Override
public boolean autenticar(int senhaFornecida) {
if (this.senha == senhaFornecida) {
System.out.println("Usuário " + this.login + " autenticado com sucesso.");
logAcesso(); // Chama o método default da interface
return true;
} else {
System.out.println("Falha na autenticação para o usuário " + this.login + ".");
return false;
}
}
}
class SistemaInterno {
public void login(Autenticavel objAutenticavel, int senha) {
System.out.println("\nTentando login no sistema interno...");
if (objAutenticavel.autenticar(senha)) {
System.out.println("Bem-vindo ao sistema!");
} else {
System.out.println("Acesso negado.");
}
}
}
public class TesteInterface {
public static void main(String[] args) {
Autenticavel.exibirRegrasSenha(); // Chamando método static da interface
Usuario user1 = new Usuario("joao.dev", 1234);
Usuario user2 = new Usuario("maria.qa", Autenticavel.SENHA_PADRAO); // Usando constante da interface
SistemaInterno si = new SistemaInterno();
si.login(user1, 1234);
si.login(user2, 9999); // Senha errada
si.login(user2, Autenticavel.SENHA_PADRAO); // Senha correta
}
}
### Escolhendo entre Classe Abstrata e Interface:
Classe Abstrata: Utilize se: Houver necessidade de compartilhar código (implementação de métodos ou atributos) entre diversas classes com forte relação. As classes que herdarem dela possuírem muitos métodos ou atributos em comum, ou necessitarem de acesso não público (como protected) a esses membros. Quando for preciso definir um estado (variáveis de instância) compartilhado por todas as subclasses.
Interface: Utilize se: Classes sem relação hierárquica precisarem implementar a interface (ex: Comparable, Serializable, Runnable). O objetivo for especificar o comportamento de um tipo de dado, sem importar a classe que o implementará. Se for desejável aproveitar a herança múltipla de tipos.
Composição vs. Herança: Uma Escolha Estratégica para Reutilização e Flexibilidade
A herança estabelece uma relação do tipo "É-UM", promovendo a reutilização de código e o polimorfismo. No entanto, essa abordagem gera um forte acoplamento entre a superclasse e suas subclasses. Em contraste, a composição modela uma relação "TEM-UM" (ou "parte-de"), onde uma classe ("o todo") contém instâncias de outras classes ("as partes") como seus membros. Por exemplo, um Computador "TEM-UM" Processador e "TEM-UMA" Memoria, enquanto uma Conta "TEM-UM" Cliente.
Vantagens da Composição sobre a Herança:
Maior Flexibilidade: As dependências ("as partes") podem ser modificadas em tempo de execução, permitindo o uso de diferentes implementações para essas "partes". Menor Acoplamento: A classe "o todo" interage com as "partes" através de suas interfaces públicas. Alterações internas em uma "parte" geralmente não afetam "o todo", desde que a interface da "parte" permaneça a mesma.
Evita Problemas da Herança: Contribui para evitar problemas como a "base frágil" (onde mudanças na superclasse podem prejudicar as subclasses) e a complexidade da herança múltipla (não suportada em Java para classes).
Modela Relações Dinâmicas: Permite representar relações mais complexas e que podem evoluir dinamicamente entre os objetos.
Quando Optar pela Herança
A herança é mais adequada para relações genuínas de "É-UM", onde a subclasse é verdadeiramente uma especialização da superclasse e o Princípio da Substituição de Liskov (LSP) pode ser aplicado (ou seja, um objeto da subclasse pode substituir um objeto da superclasse sem comprometer a correção do programa).
É útil para especializar comportamentos e em padrões como o Template Method.
Quando Priorizar a Composição:
A composição é geralmente a opção preferível para a reutilização de código quando uma relação "É-UM" não é claramente aplicável. É ideal para construir objetos complexos a partir de componentes menores e independentes, visando maior flexibilidade e evitando os problemas associados à herança de classes. O conhecido princípio de design "favoreça a composição sobre a herança" reflete essa preferência em muitos cenários.A valorização da "composição sobre a herança" e da "programação para interfaces" são mais do que simples recomendações; são estratégias cruciais para desenvolver software adaptável e resiliente. A decisão entre usar uma classe abstrata ou uma interface, ou entre herança e composição, deve ser orientada pelo contexto específico do problema e pelos requisitos de design. No entanto, uma tendência geral em direção às opções que maximizam a flexibilidade e minimizam o acoplamento é uma prática recomendada, especialmente em sistemas que se espera que evoluam ao longo do tempo. Essa abordagem prepara o caminho para a introdução e aplicação eficaz dos princípios SOLID, que formalizam muitas dessas boas práticas de design, com o objetivo de criar software robusto, de fácil manutenção e extensível. A capacidade de adaptar-se a mudanças com impacto e custo mínimos é uma característica fundamental de um bom design de software, e as escolhas relativas à abstração e composição são essenciais para alcançar essa adaptabilidade.
Top comments (0)