Sistema de Evolução de Personagem (RPG)

Trabalho Final de Programação Orientada a Objetos I

Professor: Davi Taveira Alencar Alarcão

Alunos: Aline Alves de Oliveira & Lucas Ferreira Fernandes Batista

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:

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:

Mecanismos de Tratamento: `try-catch-finally`

Para capturar e tratar erros de forma elegante, utilizamos os blocos `try-catch-finally`:


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`:


// 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.");
}
            

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.

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)

Vídeo de Apresentação