Portal do Projeto
Todos os materiais do projeto estão centralizados aqui. Use os links abaixo para acessar o código-fonte, o relatório detalhado e o vídeo de apresentação.
Esta seção explica os conceitos de POO que fundamentaram nosso projeto, com exemplos práticos retirados diretamente do nosso código.
Classes Abstratas e Interfaces (Os Pilares da Abstração)
Classes Abstratas: O Molde do Personagem
Uma classe abstrata é como a planta de um carro: ela define as partes essenciais, mas você não pode dirigir a planta. Você precisa construir um carro real a partir dela. No nosso projeto, `Evoluivel` é a nossa classe abstrata. Ela define que todo personagem terá `nome`, `nivel` e `experiencia`, e força as classes filhas a implementarem o método `descreverPersonagem()`.
// A "planta" do personagem. Não pode ser instanciada.
public abstract class Evoluivel {
protected String nome;
protected int nivel;
protected int experiencia;
public Evoluivel(String nome) {
this.nome = nome;
this.nivel = 1;
this.experiencia = 0;
}
// Método concreto: a lógica é a mesma para todos.
public void ganharExperiencia(int pontos) {
this.experiencia += pontos;
}
// Método abstrato: força as classes filhas a implementarem.
public abstract String descreverPersonagem();
}
Interfaces: O Contrato de Habilidade
Uma interface é como uma "certificação". Ela não diz o que você é, mas garante que você tem a habilidade de fazer algo. No nosso caso, a interface `Upgradavel` garante que qualquer classe que a implemente terá a capacidade de `subirNivel()`. Isso nos dá flexibilidade para que, no futuro, até mesmo itens possam subir de nível, não apenas personagens.
// O "contrato" que define a habilidade de subir de nível.
public interface Upgradavel {
void subirNivel() throws NivelMaximoAtingidoException,
ExperienciaInsuficienteException;
}
// A classe abstrata implementa a interface, passando a
// responsabilidade para as classes concretas.
public abstract class Evoluivel implements Upgradavel {
// ... corpo da classe
}
Polimorfismo: A Mágica em Ação
Polimorfismo significa "muitas formas". É a consequência poderosa de usar herança e interfaces. Ele nos permite tratar objetos diferentes (`Guerreiro`, `Mago`) da mesma maneira, desde que compartilhem uma base comum (`Evoluivel`). Isso simplifica o código drasticamente.
// Podemos tratar objetos de tipos diferentes da mesma forma.
Evoluivel aragorn = new Guerreiro("Aragorn");
Evoluivel gandalf = new Mago("Gandalf");
// O método chamado é o específico de cada classe (Guerreiro ou Mago).
System.out.println(aragorn.descreverPersonagem());
System.out.println(gandalf.descreverPersonagem());
Classe Abstrata vs. Interface: A Escolha Estratégica
A decisão entre usar uma classe abstrata e uma interface depende da natureza do que você está modelando e das necessidades do seu sistema. Ambas promovem a abstração e o polimorfismo, mas de maneiras diferentes:
- Classe Abstrata (`Evoluivel`): Utilizamos `Evoluivel` como uma classe abstrata porque ela define um "é um" relacionamento forte. Todos os personagens (Guerreiro, Mago, etc.) são um tipo de `Evoluivel`. A classe abstrata nos permitiu compartilhar atributos comuns (nome, nível, experiência) e implementar métodos padrão (`ganharExperiencia`) que são válidos para todos os personagens, enquanto delegamos a implementação de métodos específicos (`descreverPersonagem`) para as subclasses. Isso evita duplicação de código e garante uma base consistente.
- Interface (`Upgradavel`): Optamos por uma interface para `Upgradavel` porque ela define uma "pode fazer" capacidade. Nem tudo no nosso jogo é um personagem, mas muitas coisas podem ser atualizadas. Ao usar uma interface, mantemos a flexibilidade para que, no futuro, itens, equipamentos ou até mesmo habilidades possam implementar `Upgradavel` e ter a lógica de `subirNivel()`, sem a necessidade de serem hierarquicamente relacionados a `Evoluivel`. Isso promove um design mais flexível e desacoplado.
Em resumo, a classe abstrata foi usada para definir a base comum e o comportamento padrão dos personagens, enquanto a interface foi empregada para declarar uma capacidade que pode ser compartilhada por diversas entidades no jogo, independentemente de sua hierarquia de classes.
Tratamento de Exceções: Construindo Software Seguro
Um programa robusto sabe como lidar com falhas de forma elegante. Em vez de depender de exceções genéricas, podemos criar as nossas próprias para representar as regras de negócio do jogo, como `ExperienciaInsuficienteException`. Isso torna os erros mais claros e o tratamento mais específico, resultando em um código mais seguro e fácil de manter.
Hierarquia de Erros: `Error` vs. `Exception`
Em Java, a hierarquia de erros é dividida em duas categorias principais:
- `Error`: Representa problemas graves que geralmente indicam falhas irrecuperáveis do sistema, como falta de memória (`OutOfMemoryError`) ou erros na máquina virtual Java (`VirtualMachineError`). Geralmente, você não deve tentar tratar `Error`s diretamente, pois eles sinalizam condições que estão fora do controle do seu programa.
- `Exception`: Representa condições excepcionais que um programa pode (e deve) tentar capturar e tratar. Existem dois tipos de exceções: checadas e não checadas.
Exceções Checadas (`Checked`) vs. Não Checadas (`Unchecked`)
A escolha entre exceções checadas e não checadas tem um impacto significativo na forma como você escreve e trata os erros:
- Exceções Checadas (`Checked Exceptions`): São exceções que o compilador força você a tratar ou declarar. Elas herdam diretamente da classe `Exception` (mas não de `RuntimeException`). Exemplos comuns incluem `IOException` (erro de entrada/saída) ou `SQLException` (erro de banco de dados).
- Impacto no código: Métodos que podem lançar exceções checadas devem usar a palavra-chave `throws` na sua assinatura, e o código que chama esses métodos deve envolvê-los em um bloco `try-catch` ou também declarar que lança a exceção. Isso garante que o desenvolvedor esteja ciente e planeje o tratamento dessas possíveis falhas.
- Quando usar: São ideais para condições excepcionais que o programa pode prever e que exigem uma decisão de recuperação por parte do chamador. Por exemplo, se um arquivo não for encontrado, o programa pode tentar criar um novo ou informar o usuário.
- Exceções Não Checadas (`Unchecked Exceptions`): São exceções que o compilador não força você a tratar ou declarar. Elas herdam da classe `RuntimeException`. Exemplos incluem `NullPointerException` (tentar acessar um objeto nulo) ou `ArrayIndexOutOfBoundsException` (tentar acessar um índice inválido em um array).
- Impacto no código: Não há requisito de `try-catch` ou `throws` explícito, embora você possa tratá-las se desejar.
- Quando usar: São apropriadas para erros de programação ou falhas que indicam um bug no código. Por exemplo, se você tentar dividir por zero, isso indica um problema lógico que deve ser corrigido, não uma condição que o programa deva tentar se recuperar elegantemente.
Mecanismos de Tratamento: `try-catch-finally`
Para capturar e tratar erros de forma elegante, utilizamos os blocos `try-catch-finally`:
- `try`: Contém o código que pode potencialmente lançar uma exceção.
- `catch`: Bloco executado se uma exceção específica for lançada dentro do bloco `try`. Você pode ter múltiplos blocos `catch` para tratar diferentes tipos de exceções.
- `finally`: Opcional, este bloco é sempre executado, independentemente de uma exceção ter sido lançada ou não. É ideal para liberar recursos, como fechar conexões de banco de dados ou streams de arquivo, garantindo que a limpeza seja sempre realizada.
try {
// Código que pode lançar uma ExperienciaInsuficienteException
gandalf.subirNivel();
} catch (ExperienciaInsuficienteException e) {
// Capturando o erro de forma elegante
System.err.println("AVISO: " + e.getMessage());
// Outras ações de tratamento, como registrar o erro ou notificar o usuário
} catch (IOException e) {
// Tratando outras exceções, se aplicável
System.err.println("Erro de E/S: " + e.getMessage());
} finally {
// Este bloco é sempre executado, útil para limpeza de recursos
System.out.println("Processo de nivelamento finalizado.");
}
Lançando Exceções: `throw` e `throws`
Para sinalizar problemas no seu programa, usamos as palavras-chave `throw` e `throws`:
- `throw`: Usada para lançar explicitamente uma exceção de dentro de um método ou bloco de código. Quando uma exceção é lançada, a execução normal do programa é interrompida e o controle é transferido para o bloco `catch` mais próximo que possa lidar com essa exceção.
// Lançando nossa exceção personalizada dentro do método subirNivel()
if (this.experiencia < XP_PARA_PROXIMO_NIVEL) {
throw new ExperienciaInsuficienteException("XP insuficiente para o próximo nível.");
}
- `throws`: Usada na assinatura de um método para declarar que ele pode lançar uma ou mais exceções checadas. Isso informa ao compilador e a quem chama o método que essa exceção precisa ser tratada.
public void realizarSaque(double valor) throws SaldoInsuficienteException {
if (this.saldo < valor) {
throw new SaldoInsuficienteException("Saldo insuficiente para realizar o saque.");
}
this.saldo -= valor;
}
Criando Exceções Personalizadas
A importância de criar seus próprios tipos de erro, como `ExperienciaInsuficienteException` ou `SaldoInsuficienteException`, é fundamental para tornar o sistema mais claro e fácil de dar manutenção.
- Clareza e Especificidade: Exceções personalizadas permitem que você represente condições de erro específicas do seu domínio de negócio. Em vez de usar um genérico `IllegalArgumentException`, uma `ExperienciaInsuficienteException` imediatamente diz ao desenvolvedor qual é o problema.
- Facilidade de Manutenção: Ao ter exceções bem definidas, o código de tratamento de erros se torna mais legível e fácil de depurar. Você pode ter blocos `catch` específicos para cada tipo de problema, permitindo um tratamento mais preciso.
- Encapsulamento de Regras de Negócio: Suas exceções personalizadas podem carregar informações adicionais relevantes para o erro, como o valor da experiência atual ou o saldo disponível, que podem ser úteis para o tratamento ou log.
Para criar uma exceção personalizada, basta que ela herde de `Exception` (para checadas) ou `RuntimeException` (para não checadas):
// Exemplo de exceção personalizada checada
public class ExperienciaInsuficienteException extends Exception {
public ExperienciaInsuficienteException(String message) {
super(message);
}
}
// Exemplo de exceção personalizada não checada (se fosse o caso)
public class SaldoInsuficienteException extends RuntimeException {
public SaldoInsuficienteException(String message) {
super(message);
}
}
Ao adotar uma abordagem estruturada para o tratamento de exceções, com o uso de `try-catch-finally`, o lançamento adequado de exceções e a criação de tipos personalizados, você constrói software mais confiável, robusto e fácil de manter.
Relatório do Projeto
O relatório completo do projeto está disponível no seguinte link:
Acessar Relatório Completo (PDF)