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
- Tutorial de aplicativo de console — um passo a passo oficial de aplicação de console, útil para revisar o fluxo deste capstone.
- Enumerações (enum) — o tipo de valores fixos nomeados usado para a prioridade.
- Leitura e escrita de arquivos — as ferramentas de arquivo usadas na persistência.
- File.ReadAllLines e WriteAllLines — a forma simples de gravar e ler linhas de um arquivo.
- String.Split — o método usado para separar os campos de cada linha.
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.