Pular para o conteúdo

Revisão de Conceitos

Uma classe é uma definição para criar objetos. Ela define os atributos (variáveis) e comportamentos (métodos) que os objetos criados a partir dela terão.

Ela pode ser vista como um molde ou uma planta que descreve as características e comportamentos de um tipo específico de objeto. Por exemplo, uma classe Pessoa pode ter atributos como nome e idade, e métodos como apresentar(), que exibe uma mensagem de apresentação.

Uma classe pode ser composta de:

  • Atributos: Variáveis que armazenam informações sobre o estado do objeto. No exemplo da classe Pessoa, os atributos são nome e idade.
  • Métodos: Funções que definem o comportamento do objeto. No exemplo da classe Pessoa, o método é apresentar(), que exibe uma mensagem de apresentação.
public class Pessoa {
private String nome;
private int idade;
public Pessoa(String nome, int idade) {
this.nome = nome;
this.idade = idade;
}
public void apresentar() {
System.out.println("Olá, meu nome é " + nome + " e tenho " + idade + " anos.");
}
}

Já um objeto é uma instância de uma classe. Ele é criado a partir da definição da classe e possui seus próprios valores para os atributos definidos na classe. Por exemplo, se criarmos um objeto pessoa1 a partir da classe Pessoa, ele terá seus próprios valores para nome e idade, e poderá chamar o método apresentar() para exibir sua apresentação.

Pessoa pessoa1 = new Pessoa("Alice", 30);
pessoa1.apresentar(); // Saída: Olá, meu nome é Alice tenho 30 anos.

Cada objeto criado a partir de uma classe é independente dos outros objetos, mesmo que sejam instâncias da mesma classe. Por exemplo, se criarmos outro objeto pessoa2 a partir da classe Pessoa, ele terá seus próprios valores para nome e idade, e poderá chamar o método apresentar() para exibir sua apresentação, sem afetar o objeto pessoa1.

Interfaces em Java definem um contrato: um conjunto de métodos que uma classe deve implementar, sem definir como esses métodos serão executados. Elas ajudam a desacoplar código e permitem múltiplas “tipagens” para o mesmo objeto.

  • Não possuem implementação concreta (até o Java 7, ver exceções abaixo).
  • Uma classe implementa uma interface usando implements.
  • Uma classe pode implementar múltiplas interfaces (diferente de herança de classe única).
  • Servem para definir papéis/comportamentos, e não detalhes de implementação.
public interface Notificador {
void notificar(String mensagem);
}

Qualquer classe que implementar Notificador deve fornecer uma implementação para notificar.

public class EmailNotificador implements Notificador {
@Override
public void notificar(String mensagem) {
System.out.println("Enviando e-mail: " + mensagem);
}
}
public class SmsNotificador implements Notificador {
@Override
public void notificar(String mensagem) {
System.out.println("Enviando SMS: " + mensagem);
}
}

Uso:

public class Pedido {
private Notificador notificador;
public Pedido(Notificador notificador) {
this.notificador = notificador;
}
public void confirmar() {
System.out.println("Pedido confirmado.");
notificador.notificar("Seu pedido foi confirmado!");
}
}
// Em outro ponto do código:
Notificador notificador = new EmailNotificador();
Pedido pedido = new Pedido(notificador);
pedido.confirmar();

Repare que Pedido não sabe se a notificação é por e-mail, SMS etc. Ele depende apenas da interface Notificador, reduzindo acoplamento.

A partir do Java 8, interfaces podem ter:

  • Métodos default: implementações padrão que podem ser reutilizadas ou sobrescritas.
public interface Validavel {
boolean isValido();
default void imprimirEstadoValidacao() {
if (isValido()) {
System.out.println("Objeto válido.");
} else {
System.out.println("Objeto inválido.");
}
}
}
  • Métodos static: funções utilitárias relacionadas ao contrato.
public interface Conversor {
static double celsiusParaFahrenheit(double c) {
return c * 1.8 + 32;
}
}

Muitos padrões de projeto (Strategy, Observer, Factory Method, Adapter, etc.) fazem uso intenso de interfaces para:

  • Definir pontos de extensão.
  • Permitir troca de implementações em tempo de execução.
  • Reduzir dependência entre módulos (baixo acoplamento).
  • Facilitar testes (mocks/stubs implementando interfaces).

Interfaces são, portanto, um dos principais mecanismos para alcançar flexibilidade e boa arquitetura em sistemas orientados a objetos.

A herança e a composição são dois conceitos fundamentais na programação orientada a objetos, usados para reutilizar código e estruturar sistemas.

A herança é um mecanismo que permite que uma classe (chamada de classe filha ou subclasse) herde atributos e métodos de outra classe (chamada de classe pai ou superclasse). Isso promove a reutilização de código e facilita a extensão de funcionalidades.

Por exemplo, considere uma classe Animal que define atributos e métodos comuns a todos os animais. Uma subclasse Cachorro pode herdar esses atributos e métodos, além de adicionar comportamentos específicos.

public class Animal {
protected String nome;
public Animal(String nome) {
this.nome = nome;
}
public void emitirSom() {
System.out.println("O animal emite um som.");
}
}
public class Cachorro extends Animal {
public Cachorro(String nome) {
super(nome);
}
@Override
public void emitirSom() {
System.out.println("O cachorro late.");
}
}

No exemplo acima, a classe Cachorro herda o atributo nome e o método emitirSom da classe Animal, mas também pode sobrescrever o método para fornecer um comportamento específico.

A composição é uma abordagem em que uma classe é composta por instâncias de outras classes, em vez de herdar diretamente de uma superclasse. Isso promove maior flexibilidade e reduz o acoplamento entre as classes.

Por exemplo, considere uma classe Motor que define o comportamento de um motor. Uma classe Carro pode ser composta por uma instância de Motor, em vez de herdar dela.

public class Motor {
public void ligar() {
System.out.println("O motor está ligado.");
}
}
public class Carro {
private Motor motor;
public Carro() {
this.motor = new Motor();
}
public void ligarCarro() {
motor.ligar();
System.out.println("O carro está ligado.");
}
}

No exemplo acima, a classe Carro utiliza a composição para incluir um Motor, permitindo maior flexibilidade na reutilização de código.

  • Herança: Promove a reutilização de código, mas pode levar a um acoplamento mais forte entre as classes. Deve ser usada quando há uma relação “é um” (ex.: um cachorro é um animal).
  • Composição: Promove maior flexibilidade e baixo acoplamento. Deve ser usada quando há uma relação “tem um” (ex.: um carro tem um motor).

Ambas as abordagens têm suas vantagens e desvantagens, e a escolha entre elas depende do contexto e dos requisitos do sistema.

Acoplamento e coesão são dois conceitos fundamentais na engenharia de software, especialmente no contexto de design de sistemas e programação orientada a objetos.

Acoplamento refere-se ao grau de dependência entre diferentes módulos ou classes de um sistema. Um sistema com baixo acoplamento é preferível, pois as mudanças em um módulo têm menos impacto nos outros.

  • Baixo Acoplamento: Indica que os módulos ou classes têm pouca dependência entre si. Isso facilita a manutenção, testes e reutilização de código.
  • Alto Acoplamento: Indica que os módulos ou classes estão fortemente interligados, o que pode dificultar a manutenção e evolução do sistema.

Exemplo de baixo acoplamento:

public interface Notificador {
void notificar(String mensagem);
}
public class EmailNotificador implements Notificador {
@Override
public void notificar(String mensagem) {
System.out.println("Enviando email: " + mensagem);
}
}
public class Pedido {
private Notificador notificador;
public Pedido(Notificador notificador) {
this.notificador = notificador;
}
public void confirmar() {
System.out.println("Pedido confirmado.");
notificador.notificar("Seu pedido foi confirmado!");
}
}

No exemplo acima, a classe Pedido depende da interface Notificador, e não de uma implementação específica, promovendo baixo acoplamento.

Coesão refere-se ao grau em que as responsabilidades de um módulo ou classe estão relacionadas. Uma classe com alta coesão realiza uma única tarefa ou um conjunto de tarefas intimamente relacionadas.

  • Alta Coesão: Indica que uma classe ou módulo tem responsabilidades bem definidas e relacionadas. Isso torna o código mais fácil de entender e manter.
  • Baixa Coesão: Indica que uma classe ou módulo realiza muitas tarefas não relacionadas, o que pode dificultar a compreensão e manutenção.

Exemplo de alta coesão:

public class Calculadora {
public int somar(int a, int b) {
return a + b;
}
public int subtrair(int a, int b) {
return a - b;
}
}

No exemplo acima, a classe Calculadora tem alta coesão, pois todas as suas responsabilidades estão relacionadas a operações matemáticas.

  • Acoplamento: Focado na relação entre diferentes classes ou módulos.
  • Coesão: Focado na organização interna de uma única classe ou módulo.

Um bom design de software busca baixo acoplamento e alta coesão, promovendo sistemas mais robustos, flexíveis e fáceis de manter.