O Projeto Final Começa — Arquitetando um Mini Banco de Dados em Memória

O Projeto Final Começa — Arquitetando um Mini Banco de Dados em Memória

O projeto final não traz conceito novo — cobra os que já apareceram, todos ao mesmo tempo. Começa pela arquitetura de um mini banco de dados em memória: quais são as peças, quem possui quem, onde ficam as fronteiras, e o esqueleto CMake que sustenta o resto, com a medida certa de planejamento antes do código.
Linguagem C++

11 min de leitura

Chegamos à Fase 9, o fechamento. Nos 38 artigos anteriores, você aprendeu os conceitos do C++ moderno um a um, e em cada aula eu insisti que "cada artigo é o curso em miniatura" — mostrando as peças trabalhando juntas em pequena escala. Agora vem a prova real: construir, do zero, uma aplicação de verdade que integre tudo. Escolhi um mini banco de dados em memória — uma ferramenta que armazena registros, consulta-os, filtra e ordena, como um SQLite minúsculo que vive na RAM. É um projeto ideal porque exercita naturalmente RAII, ponteiros inteligentes, classes, STL, templates, exceções e testes, todos num sistema coeso. Este primeiro artigo não escreve o motor ainda: ele arquiteta. Vamos decidir o que construir, como as partes se encaixam, e montar o esqueleto CMake. Projetar antes de codificar é, em si, uma das lições mais profissionais do curso.

O que vamos construir

Nosso banco de dados será simples mas real. Ele terá tabelas, cada uma com colunas tipadas e linhas de dados. Você poderá inserir registros, consultá-los com filtros ("todos com idade maior que 30"), ordená-los e removê-los. Um valor de célula poderá ser um inteiro, um número real ou um texto — um dado poliforme, que já aponta para o std::variant da Fase 6. Em uso, ele se pareceria com isto (a API que vamos projetar para construir depois):

// Visão do que queremos que funcione ao fim do capstone (ainda não implementado):
Tabela usuarios{{"nome", Tipo::Texto}, {"idade", Tipo::Inteiro}};

usuarios.inserir({{"nome", "Ana"}, {"idade", 30}});
usuarios.inserir({{"nome", "Bruno"}, {"idade", 25}});

// Consulta: todos com idade >= 28, ordenados por nome.
auto resultado = usuarios
    .filtrar([](const Linha& l) { return l.get_int("idade") >= 28; })
    .ordenar_por("nome");

for (const auto& linha : resultado)
    std::cout << linha.get_texto("nome") << '\n';

Note quanta coisa do curso está latente aí: Tabela é uma classe (Fase 3); filtrar recebe uma lambda (Fase 5); o encadeamento .filtrar(...).ordenar_por(...) usa a interface fluente com *this (Fase 3); os valores poliformes pedem variant (Fase 6); e por baixo tudo será containers da STL (Fase 4) com gestão automática de memória (Fase 2). Não vamos construir tudo isso hoje — vamos planejar como.

Arquitetura: as peças e suas responsabilidades

Bom software se divide em componentes com responsabilidades claras. Antes de escrever uma linha de lógica, definimos as peças do nosso banco e o que cada uma faz — o princípio de "funções e classes focadas" do artigo A Sabedoria Codificada — Core Guidelines e Código Limpo em C++, agora em escala de módulos:

O primeiro componente é o Valor (Value): representa uma única célula, que pode conter um inteiro, um real ou um texto. Ele encapsulará um std::variant e oferecerá acesso seguro e tipado. É a peça mais fundamental, e a construiremos no artigo A Primeira Pedra — Uma Célula Poliforme com std::variant.

O segundo é a Linha (Row): um registro, isto é, uma coleção de valores indexados por nome de coluna. Internamente será um mapa de coluna→valor.

O terceiro é a Tabela (Table): o coração do sistema, que possui as linhas, conhece o esquema (nomes e tipos das colunas), e oferece as operações de inserir, remover e consultar. Ela aplicará RAII possuindo suas linhas via containers da STL, sem gestão manual de memória. Construiremos a Tabela no artigo A Segunda Pedra — Linhas e Tabelas com RAII e STL.

O quarto é o motor de consultas: os métodos filtrar, ordenar_por e afins, que operam sobre as linhas usando os algoritmos da STL e lambdas, devolvendo novos conjuntos de resultados. Esse motor, com seus templates e interface fluente, é o artigo A Terceira Pedra — Um Motor de Consultas com Templates e Lambdas.

E o quinto, transversal, é o tratamento de erros: o que acontece quando você pede uma coluna que não existe, ou um tipo errado. Usaremos std::optional/std::expected e exceções conforme o caso, no artigo A Quarta Pedra — Tratamento de Erros Coerente com std::expected. Essa separação de responsabilidades — cada peça com um trabalho claro — é o que torna o sistema compreensível e testável.

O esqueleto CMake

Vamos materializar a arquitetura na estrutura do projeto, aplicando o CMake do artigo Do Comando Único ao Projeto Real — Construindo com CMake. Organizamos o código em uma biblioteca (minidb) com as peças do banco, um executável de demonstração, e um alvo de testes com Catch2:

# CMakeLists.txt — esqueleto do projeto minidb
cmake_minimum_required(VERSION 3.16)
project(MiniDB LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# A biblioteca com a lógica do banco (arquivos que criaremos nos próximos artigos).
add_library(minidb
    src/value.cpp
    src/table.cpp
)
target_include_directories(minidb PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

# Executável de demonstração que usa a biblioteca.
add_executable(demo src/main.cpp)
target_link_libraries(demo PRIVATE minidb)

# Testes com Catch2 (buscado automaticamente).
include(FetchContent)
FetchContent_Declare(Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.5.0)
FetchContent_MakeAvailable(Catch2)

add_executable(testes tests/test_minidb.cpp)
target_link_libraries(testes PRIVATE minidb Catch2::Catch2WithMain)
enable_testing()
add_test(NAME suite COMMAND testes)

A estrutura de pastas correspondente separa interfaces (include/), implementações (src/) e testes (tests/) — a organização profissional que discutimos. Um cabeçalho inicial declara os tipos que vamos preencher:

// include/minidb/tipos.h — os tipos fundamentais que o projeto usará
#pragma once
#include <string>

namespace minidb {

// Os tipos que uma coluna pode ter. Um enum forte (enum class) evita conversões acidentais.
enum class Tipo {
    Inteiro,
    Real,
    Texto
};

}  // namespace minidb

Usamos um namespace minidb (Fase 1) para agrupar tudo e um enum class (uma enumeração forte, que não converte implicitamente para int) para os tipos de coluna. O projeto agora tem esqueleto, arquitetura e um caminho claro.

A honestidade sobre projetar antes de codificar

Prometo a franqueza de sempre, e ela vale muito aqui. Passar um artigo inteiro planejando sem escrever lógica pode parecer improdutivo para quem tem pressa de codificar. É o oposto: decidir a arquitetura antes — quais são as peças, quem é responsável por quê, como se conectam — é o que evita o retrabalho caótico de construir sem rumo. Dito isso, há um equilíbrio honesto: planejar demais, antecipando cada detalhe de um sistema que ainda não existe, também é armadilha (a paralisia por análise), e projetos reais evoluem à medida que você os constrói. A postura madura é planejar o suficiente para ter direção clara — as peças principais e suas fronteiras —, e refinar os detalhes conforme codifica, permitindo que a arquitetura se ajuste ao que você aprende no caminho. Nosso plano de cinco componentes é esse "suficiente": firme o bastante para guiar, flexível o bastante para evoluir.

Projetar antes de codificar tem medida: o suficiente para saber quem possui o quê e quais são as fronteiras entre as peças, não tanto que se antecipem detalhes que só o código revela. Definir posse logo no início é o que evita a refatoração mais cara — decidir depois que a tabela possui as linhas, e não o contrário, costuma significar mexer em tudo. O projeto final não traz conceito novo: ele cobra os que já apareceram, juntos e no mesmo lugar.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Desenhe (em texto ou diagrama) as cinco peças do nosso banco de dados e as relações entre elas: quem possui quem, quem usa quem. Por exemplo, a Tabela possui Linhas; a Linha possui Valores.

Ver resposta

✓ Resposta: As cinco peças e suas relações, em termos de posse (tem um) e uso: - A Tabela possui um conjunto de Linhas (composição: a tabela contém suas linhas via um container da STL) e possui o esquema (nomes e tipos das colunas). - Cada Linha possui vários Valores (composição: um mapa de nome-de-coluna → Valor). - Cada Valor possui um dado poliforme (um variant de int/real/texto). - O motor de consultas não possui nada permanentemente: ele usa a Tabela e as Linhas, aplicando lambdas e algoritmos para produzir resultados (novos conjuntos de linhas). - O tratamento de erros é transversal: usado por todas as peças quando um acesso inválido ocorre (coluna inexistente, tipo errado). A cadeia de posse é: Tabela → Linhas → Valores → variant, tudo por composição, o que dá ao sistema a Regra do Zero (nenhuma gestão manual de memória).

Exercício 2

Explique por que usamos enum class Tipo (enumeração forte) em vez de um enum simples ou de constantes inteiras para representar os tipos de coluna. Que erro o enum class previne?

Ver resposta

✓ Resposta: Usamos enum class Tipo porque a enumeração forte não converte implicitamente para int nem polui o escopo circundante com seus nomes. Um enum simples (à moda C) converteria Tipo::Inteiro para 0 automaticamente, permitindo erros absurdos como comparar um Tipo com um número qualquer, passá-lo onde se espera um int, ou misturar valores de enums diferentes — bugs silenciosos. O enum class previne exatamente isso: Tipo::Inteiro é do tipo Tipo e só, exigindo conversão explícita se você realmente quiser o número; e seus enumeradores ficam qualificados (Tipo::Inteiro, não apenas Inteiro), evitando colisões de nome. É mais seguro e mais claro, alinhado à filosofia de "tornar intenções e restrições visíveis" do artigo A Sabedoria Codificada — Core Guidelines e Código Limpo em C++.

Exercício 3

Crie a estrutura de pastas do projeto (include/minidb/, src/, tests/) e o CMakeLists.txt do esqueleto, com arquivos-fonte vazios (apenas com os includes). Confirme que cmake -S . -B build configura sem erro.

Ver resposta

✓ Resposta: Estrutura e configuração (esqueleto mínimo):

projeto/
├── CMakeLists.txt
├── include/minidb/tipos.h
├── src/value.cpp        (vazio, só #include)
├── src/table.cpp        (vazio, só #include)
├── src/main.cpp         (int main(){return 0;})
└── tests/test_minidb.cpp (só #include do Catch2)

Com o CMakeLists.txt da aula e arquivos-fonte contendo apenas os includes necessários (e um main mínimo em main.cpp), rodar cmake -S . -B build deve configurar sem erro — o CMake lê a descrição, busca o Catch2 e gera o build. (Os .cpp vazios compilam; a lógica virá nos próximos artigos.) Se houver erro, os suspeitos comuns são o caminho do include (target_include_directories) ou a conexão de rede para o FetchContent do Catch2.

Exercício 4

Para cada uma das cinco peças da arquitetura, cite qual(is) fase(s) do curso ela mais exercita e por quê. Por exemplo: o Valor exercita a Fase 6 (variant) e a Fase 3 (encapsulamento).

Ver resposta

✓ Resposta: As fases que cada peça mais exercita: - Valor: Fase 6 (std::variant para o dado poliforme, exceções no acesso tipado) e Fase 3 (encapsulamento do variant atrás de uma interface segura). Também Fase 1 (const-correctness nos acessores). - Linha: Fase 4 (um std::map/unordered_map de coluna→valor) e Fase 3 (encapsulamento). - Tabela: Fase 2 (RAII e Regra do Zero, possuindo linhas sem gestão manual), Fase 4 (containers para armazenar as linhas) e Fase 3 (a classe central, com invariantes de esquema). - Motor de consultas: Fase 5 (templates e lambdas para filtros genéricos) e Fase 4 (algoritmos como sort, copy_if). Também Fase 3 (interface fluente com *this). - Tratamento de erros: Fase 6 (optional/expected para falhas esperadas, exceções para excepcionais). Isso confirma que o projeto integra o curso inteiro — cada peça é um encontro de várias fases.

Exercício 5

Discuta o trade-off entre "planejar tudo antes" e "codificar e ajustar". Descreva uma situação em que planejar demais seria prejudicial ao nosso projeto, e uma em que planejar de menos causaria retrabalho.

Ver resposta

✓ Resposta: O trade-off: planejar dá direção e evita retrabalho caótico, mas planejar em excesso desperdiça tempo antecipando detalhes que mudarão. Situação em que planejar demais seria prejudicial ao nosso projeto: gastar dias especificando, desde já, como o banco lidaria com índices, transações, persistência em disco e concorrência — recursos que não estão no escopo do capstone e cuja antecipação detalhada engessaria decisões antes de sabermos se são necessárias; melhor construir o núcleo simples primeiro e só considerar essas extensões (a trilha bônus, aliás) depois. Situação em que planejar de menos causaria retrabalho: começar a codificar a Tabela e as consultas sem primeiro definir a peça Valor e como o dado poliforme é representado — se descobríssemos no meio que a Linha e o motor de consultas assumiram representações incompatíveis do valor, teríamos de reescrever ambas para reconciliá-las. O equilíbrio que adotamos — definir as cinco peças e suas fronteiras, mas deixar os detalhes de cada uma para sua vez — é o "planejar o suficiente": direção clara sem engessamento.

Comentários

Mais em Linguagem C++

Texto como Coleção — std::string Revisitada e o std::string_view
Texto como Coleção — std::string Revisitada e o std::string_view

Enxergar std::string como container muda o que se espera dele: tamanho…

A Pedra de Acabamento — Integração, Testes e Verificação do Sistema
A Pedra de Acabamento — Integração, Testes e Verificação do Sistema

Peças que funcionam isoladas ainda podem não se encaixar, e é para isso que…

Pare de Escrever Laços — a Biblioteca <algorithm>
Pare de Escrever Laços — a Biblioteca <algorithm>

Escrever o laço à mão conta como percorrer; chamar o algoritmo diz o que se…