Projeto Capstone: Reunindo Tudo

Projeto Capstone: Reunindo Tudo

O projeto que integra o curso inteiro: um gerenciador de tarefas completo, com classe modelada por properties e construtor, coleções e consultas para filtrar e ordenar, persistência em arquivo com tratamento de exceções, e um programa principal com menu e entrada validada.
Linguagem C#

• • 14 min de leitura

Chegamos a um momento especial. Ao longo de quarenta e duas aulas, você acumulou um repertório completo: variáveis e tipos, decisões e laços, métodos, coleções, objetos com toda a sua riqueza, o LINQ, o tratamento de erros. Aprendidos um a um, esses conceitos podem parecer peças separadas. Esta aula existe para reuni-las e mostrar que, juntas, elas constroem algo real. Vamos criar, do zero e passo a passo, uma aplicação completa: um gerenciador de tarefas de linha de comando — um programa com o qual você adiciona tarefas, lista, marca como concluídas, remove, e que salva tudo para lembrar entre uma execução e outra. É modesto em escopo, mas honesto em profundidade, e cada decisão que tomarmos aplicará conscientemente algo que você aprendeu. Não há conceito novo aqui; há a síntese de tudo, e a satisfação de ver as engrenagens girarem juntas. Reproduza o código com calma, e observe seu conhecimento tomar a forma de um programa que funciona.

O que vamos construir

Nosso programa será um gerenciador de tarefas interativo, rodando no terminal. Ele oferecerá um menu com opções: adicionar uma tarefa (com título e prioridade), listar as tarefas pendentes ordenadas, marcar uma como concluída, remover uma, e sair. E, crucialmente, ele salvará as tarefas num arquivo, de modo que, ao reabrir o programa, elas ainda estejam lá. Vamos construí-lo em camadas: primeiro a tarefa (o objeto que representa cada afazer), depois o gerenciador (que cuida da coleção de tarefas), e por fim o programa principal (que conversa com o usuário). Ao longo do caminho, indicarei entre parênteses de qual aula vem cada ideia.

Passo 1: a classe Tarefa

Começamos modelando o que é uma tarefa. Cada tarefa tem um título, uma prioridade e um estado (concluída ou não). Para a prioridade, que só admite alguns valores fixos (baixa, média, alta), usaremos um enum — um recurso que representa um conjunto fixo de valores nomeados, e que se explica sozinho ao ser visto. A tarefa é uma classe com properties, validação no construtor, e comportamento próprio:

// Um enum: um tipo com valores fixos e nomeados.
enum Prioridade { Baixa, Media, Alta }

class Tarefa
{
    public string Titulo { get; }                    // property só-leitura (Aula #27)
    public Prioridade Prioridade { get; }
    public bool Concluida { get; private set; }       // só a classe altera (Aula #27)

    // Construtor que valida o título no nascimento (Aula #25):
    public Tarefa(string titulo, Prioridade prioridade)
    {
        if (string.IsNullOrWhiteSpace(titulo))       // validação (Aula #09, #25)
            throw new ArgumentException("O título não pode ser vazio.");  // exceção (Aula #40)
        Titulo = titulo;
        Prioridade = prioridade;
    }

    public void Concluir()                            // método de objeto (Aula #26)
    {
        Concluida = true;
    }

    // Devolve uma linha legível representando a tarefa:
    public string Descrever()
    {
        string marca = Concluida ? "[x]" : "[ ]";     // operador condicional (Aula #21)
        return $"{marca} {Titulo} (prioridade: {Prioridade})";  // interpolação (Aula #09)
    }
}

Cada linha carrega uma lição da jornada. O enum Prioridade dá segurança à prioridade — é impossível criar uma prioridade inválida. As properties encapsulam o estado, com Titulo sendo só-leitura e Concluida só alterável por dentro da classe (Aula #27), pela via controlada do método Concluir(). O construtor valida e lança uma exceção se o título for vazio (Aula #40), garantindo que nenhuma tarefa nasça sem título (Aula #25). E Descrever() produz uma representação legível usando o operador condicional e a interpolação. É a orientação a objetos das Fases 5 e 6 num tipo pequeno e sólido.

Passo 2: o gerenciador de tarefas

Agora a classe que cuida da coleção de tarefas, com a lógica de adicionar, listar ordenado, concluir e remover. Aqui entram as coleções e o LINQ:

class GerenciadorTarefas
{
    private List<Tarefa> _tarefas = new();   // coleção genérica, privada (Aula #20, #27)

    public void Adicionar(Tarefa tarefa)
    {
        _tarefas.Add(tarefa);                // Aula #20
    }

    // Retorna as pendentes, ordenadas por prioridade (Alta primeiro) — LINQ (Aula #37):
    public IEnumerable<Tarefa> Pendentes()
    {
        return _tarefas
            .Where(t => !t.Concluida)                    // filtra as não concluídas
            .OrderByDescending(t => t.Prioridade);       // ordena por prioridade
    }

    // Conta quantas estão pendentes — agregação LINQ (Aula #37):
    public int TotalPendentes()
    {
        return _tarefas.Count(t => !t.Concluida);
    }

    // Conclui a tarefa cujo título corresponde; devolve se encontrou:
    public bool Concluir(string titulo)
    {
        // Busca a primeira tarefa com aquele título (LINQ, Aula #37):
        Tarefa? tarefa = _tarefas.FirstOrDefault(
            t => t.Titulo.ToLower() == titulo.ToLower());   // ignora maiúsculas (Aula #09)

        if (tarefa == null) return false;    // não encontrou (tratamento de nulo, Aula #41)
        tarefa.Concluir();
        return true;
    }

    public bool Remover(string titulo)
    {
        int removidas = _tarefas.RemoveAll(
            t => t.Titulo.ToLower() == titulo.ToLower());
        return removidas > 0;
    }
}

Este gerenciador é o LINQ da Aula #37 em pleno uso: Where para filtrar pendentes, OrderByDescending para ordenar por prioridade, Count para as estatísticas, FirstOrDefault para buscar. A lista _tarefas é privada (Aula #27), protegida do acesso direto de fora — o mundo externo só interage com ela pelos métodos controlados. E Pendentes() devolve IEnumerable<Tarefa> (Aula #36), expondo a sequência sem revelar a lista interna. Note também o tratamento de nulo (Aula #41): FirstOrDefault pode devolver nulo se nada corresponder, e verificamos isso antes de usar. É a soma das fases de coleções, LINQ e robustez trabalhando junta.

Passo 3: salvando e carregando em arquivo

Nosso gerenciador guarda tarefas em memória, que somem ao fechar o programa. Para persistir, salvamos as tarefas num arquivo de texto e as carregamos ao iniciar. Ler e escrever arquivos é uma operação que pode falhar (o arquivo pode não existir, por exemplo), então usaremos tratamento de erros:

using System.IO;   // onde vivem as ferramentas de arquivo

class ArmazenamentoTarefas
{
    private string _caminho;
    public ArmazenamentoTarefas(string caminho) { _caminho = caminho; }

    // Salva cada tarefa como uma linha de texto no arquivo:
    public void Salvar(IEnumerable<Tarefa> tarefas)
    {
        List<string> linhas = new();
        foreach (Tarefa t in tarefas)   // percorre as tarefas (Aula #14)
        {
            // Formato simples: título|prioridade|concluida
            linhas.Add($"{t.Titulo}|{t.Prioridade}|{t.Concluida}");
        }
        File.WriteAllLines(_caminho, linhas);   // grava todas as linhas no arquivo
    }

    // Carrega as tarefas do arquivo, reconstruindo os objetos:
    public List<Tarefa> Carregar()
    {
        List<Tarefa> tarefas = new();
        if (!File.Exists(_caminho))    // se o arquivo ainda não existe, retorna lista vazia
            return tarefas;

        string[] linhas = File.ReadAllLines(_caminho);   // lê todas as linhas
        foreach (string linha in linhas)
        {
            string[] partes = linha.Split('|');   // separa os campos (Aula #09, texto)
            var prioridade = Enum.Parse<Prioridade>(partes[1]);  // texto → enum
            Tarefa t = new Tarefa(partes[0], prioridade);
            if (partes[2] == "True") t.Concluir();   // restaura o estado concluído
            tarefas.Add(t);
        }
        return tarefas;
    }
}

Aqui convergem várias fases. As ferramentas File.WriteAllLines e File.ReadAllLines gravam e leem todas as linhas de um arquivo de uma vez — a forma mais simples de persistir texto. O Split('|') separa cada linha nos seus campos (manipulação de texto, Aula #09). E note o cuidado: se o arquivo não existe (primeira execução), Carregar retorna uma lista vazia em vez de quebrar. A persistência real, que parecia distante, revela-se a combinação de coleções, texto e cuidado com erros que você já domina. (O Enum.Parse<Prioridade> converte o texto de volta no valor do enum — uma conversão como as que vimos, aplicada a enums.)

Passo 4: o programa principal

Por fim, o programa que junta tudo e conversa com o usuário. Ele usa o menu com switch, os laços, a entrada e saída, e amarra as camadas:

var armazenamento = new ArmazenamentoTarefas("tarefas.txt");
var gerenciador = new GerenciadorTarefas();

// Carrega as tarefas salvas anteriormente:
foreach (Tarefa t in armazenamento.Carregar())
    gerenciador.Adicionar(t);

Console.WriteLine("=== Gerenciador de Tarefas ===");
bool rodando = true;

while (rodando)   // laço principal do programa (Aula #13)
{
    Console.Write("\n[a]dicionar, [l]istar, [c]oncluir, [r]emover, [s]air: ");
    string opcao = Console.ReadLine()?.Trim().ToLower() ?? "";   // trata nulo (Aula #41)

    try   // envolve as ações em tratamento de erros (Aula #40)
    {
        switch (opcao)   // menu com switch (Aula #12)
        {
            case "a":
                Console.Write("Título: ");
                string titulo = Console.ReadLine() ?? "";
                Console.Write("Prioridade [Baixa/Media/Alta]: ");
                var prioridade = Enum.Parse<Prioridade>(Console.ReadLine() ?? "Baixa");
                gerenciador.Adicionar(new Tarefa(titulo, prioridade));
                Console.WriteLine("Tarefa adicionada.");
                break;

            case "l":
                var pendentes = gerenciador.Pendentes().ToList();   // materializa (Aula #37)
                if (pendentes.Count == 0)
                    Console.WriteLine("Nenhuma tarefa pendente.");
                foreach (Tarefa t in pendentes)
                    Console.WriteLine(t.Descrever());
                Console.WriteLine($"Total pendentes: {gerenciador.TotalPendentes()}");
                break;

            case "c":
                Console.Write("Título a concluir: ");
                bool concluiu = gerenciador.Concluir(Console.ReadLine() ?? "");
                Console.WriteLine(concluiu ? "Concluída." : "Não encontrada.");
                break;

            case "r":
                Console.Write("Título a remover: ");
                bool removeu = gerenciador.Remover(Console.ReadLine() ?? "");
                Console.WriteLine(removeu ? "Removida." : "Não encontrada.");
                break;

            case "s":
                rodando = false;
                break;

            default:
                Console.WriteLine("Opção inválida.");
                break;
        }
    }
    catch (Exception erro)   // captura erros (ex.: título vazio, prioridade inválida)
    {
        Console.WriteLine($"Erro: {erro.Message}");
    }

    armazenamento.Salvar(gerenciador.Pendentes());   // salva após cada ação
}

Console.WriteLine("Até logo!");

Este arquivo é a orquestra completa. O laço while (Aula #13) mantém o programa rodando; o switch (Aula #12) reage à opção escolhida; a entrada e saída (Aula #07) conversam com o usuário; o try/catch (Aula #40) captura erros — como um título vazio, que faz o construtor da Tarefa lançar uma exceção — e os trata com elegância, sem derrubar o programa; e os operadores de nulo (??, Aula #41) protegem contra entradas nulas. As camadas se conectam: o programa principal usa o gerenciador, que usa as tarefas, e o armazenamento persiste tudo. Rode-o com dotnet run, adicione algumas tarefas, saia, e reabra: elas continuarão lá. Você construiu um programa de verdade, do início ao fim.

A retrospectiva: veja o que você construiu

Pare e olhe o todo. Um sistema pequeno, mas que usa conscientemente quase tudo o que aprendeu: variáveis e tipos, enum, decisões (if, switch) e laços (while, foreach), métodos com parâmetros e retorno, coleções (List) e LINQ (Where, OrderBy, Count, FirstOrDefault), objetos com properties, construtores validadores, encapsulamento e comportamentos, tratamento de exceções, cuidado com nulos, e o operador condicional. Nenhuma peça está ali por acaso, e todas colaboram para um programa coeso que resolve um problema real. Se você conseguiu acompanhar e reproduzir este projeto, entendendo o papel de cada parte, então você não sabe apenas conceitos isolados — você sabe programar. Essa é a prova, e a recompensa, de toda a jornada até aqui. Um bom exercício final é imaginar como você acrescentaria um recurso novo — uma data de vencimento para as tarefas, digamos. Perceber que você saberia por onde começar, e que peças usar, é o sinal de que a linguagem, agora, é sua.

O gerenciador de tarefas está de pé, e com ele a prova de que as peças formam um todo. A classe Tarefa nasceu de properties, construtor e validação; o gerenciador usou coleções e consultas para filtrar e ordenar; a persistência em arquivo trouxe tratamento de exceções; e o programa principal reuniu tudo num laço de menu com entrada tratada com segurança.

Nada de conceitualmente novo apareceu aqui, e esse é justamente o ponto. A distância entre conhecer recursos isolados e construir um sistema coeso é grande, e ela se atravessa fazendo — decidindo onde cada responsabilidade mora, como as camadas conversam e o que acontece quando algo dá errado.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Monte o projeto completo (as classes Tarefa, GerenciadorTarefas, ArmazenamentoTarefas e o programa principal) e execute-o. Adicione tarefas, liste, conclua e remova. Feche o programa e reabra-o, confirmando que as tarefas persistiram no arquivo.

Ver resposta

✓ Resposta: Ao montar as quatro partes e executar, o menu aparece; adicionando tarefas, elas passam a constar na listagem, podem ser concluídas e removidas, e o arquivo tarefas.txt é gravado. Fechando e reabrindo o programa, o Carregar lê o arquivo e as tarefas reaparecem — confirmando a persistência. Se algo não persistir, o ponto a verificar é se o Salvar está sendo chamado após as ações e se o Carregar está sendo usado ao iniciar.

Exercício 2

Identifique, no código do capstone, um uso concreto de cada um dos seguintes conceitos, apontando onde ele aparece: (a) encapsulamento (Aula #27); (b) LINQ (Aula #37); (c) tratamento de exceção (Aula #40); (d) tratamento de nulo (Aula #41).

Ver resposta

✓ Resposta: (a) Encapsulamento: a lista _tarefas no GerenciadorTarefas é private, e o Concluida da Tarefa tem private set — o estado é protegido e só alterado por métodos controlados. (b) LINQ: o método Pendentes() usa Where(t => !t.Concluida).OrderByDescending(t => t.Prioridade) para filtrar e ordenar. (c) Tratamento de exceção: o try/catch no programa principal captura erros das ações (como título vazio), e o construtor da Tarefa lança ArgumentException. (d) Tratamento de nulo: no Concluir, o FirstOrDefault pode devolver nulo, e verificamos if (tarefa == null) antes de usar; e no menu, Console.ReadLine() ?? "" protege contra entrada nula.

Exercício 3

Adicione ao GerenciadorTarefas um método IEnumerable<Tarefa> PorPrioridade(Prioridade p) que retorne, usando LINQ, apenas as tarefas pendentes de uma dada prioridade. Depois, acrescente uma opção no menu para usá-lo.

Ver resposta

✓ Resposta: Exemplo:

// No GerenciadorTarefas:
public IEnumerable<Tarefa> PorPrioridade(Prioridade p)
{
    return _tarefas.Where(t => !t.Concluida && t.Prioridade == p);
}

E, no menu, uma opção que peça a prioridade, converta com Enum.Parse<Prioridade> e exiba os resultados de PorPrioridade. O método reaproveita o LINQ (Where) para combinar duas condições: pendente e da prioridade escolhida.

Exercício 4

O programa principal captura exceções com um try/catch. Teste isso deliberadamente: ao adicionar uma tarefa, deixe o título vazio. O que acontece, e por quê? Explique como o construtor da Tarefa e o catch do programa principal colaboram para tratar esse caso sem derrubar o programa.

Ver resposta

✓ Resposta: Ao deixar o título vazio na adição, o construtor da Tarefa — que valida com string.IsNullOrWhiteSpace — lança uma ArgumentException com a mensagem "O título não pode ser vazio.". Essa exceção interrompe a criação da tarefa, mas, como a chamada está dentro do try do programa principal, ela é capturada pelo catch (Exception erro), que exibe "Erro: O título não pode ser vazio." e permite que o programa continue rodando, voltando ao menu. Assim, o construtor e o catch colaboram: o construtor detecta e sinaliza a violação da regra (garantindo que nenhuma tarefa inválida seja criada), e o catch trata o sinal com elegância, informando o usuário sem derrubar o programa. A validação protege a integridade dos dados, e o tratamento de exceção protege a continuidade do programa.

Exercício 5

Sem código: escreva sua própria retrospectiva do projeto. Quais três conceitos do curso você sentiu serem mais essenciais para este capstone, e por quê? Como a divisão em "camadas" (Tarefa, Gerenciador, Armazenamento, Programa) ajudou a organizar o programa?

Ver resposta

✓ Resposta: (Resposta pessoal; um bom exemplo.) Três conceitos essenciais poderiam ser: os objetos (Fase 5), porque a Tarefa como classe com estado e comportamento é o alicerce de tudo — sem ela, os dados estariam soltos; o LINQ (Aula #37), porque tornou simples e legível filtrar e ordenar as tarefas, tarefas que com laços manuais seriam mais verbosas; e o tratamento de erros (Aula #40), porque foi o que permitiu o programa reagir a entradas inválidas sem quebrar, dando robustez. A divisão em camadas ajudou porque cada parte tem uma responsabilidade clara e isolada: a Tarefa sabe o que é uma tarefa; o GerenciadorTarefas cuida da coleção e das regras; o ArmazenamentoTarefas cuida de salvar e carregar; e o programa principal conversa com o usuário. Essa separação tornou cada parte mais simples de entender e de modificar isoladamente, e deixou o programa principal legível, pois ele apenas coordena as camadas em vez de fazer tudo de uma vez.

Comentários

Mais em Linguagem C#

Autenticação: Login e Áreas Protegidas
Autenticação: Login e Áreas Protegidas

Como proteger uma aplicação web: a distinção entre autenticação e autorização…

A List: Uma Coleção que Cresce e Encolhe
A List: Uma Coleção que Cresce e Encolhe

A coleção que resolve a rigidez do array: a List, que cresce e encolhe…

Validação e Robustez em APIs
Validação e Robustez em APIs

O princípio inegociável de qualquer API: nunca confie na entrada. A aula cobre…