Partilhar: WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC04372

Sebenta · Programar com código generativo (UC04372)

Programação genérica, geração de código, design patterns, metaprogramação e segurança, aplicados a conteúdos interativos
25h · 2.25 pontos crédito Curso: T. Produção de Conteúdos I ↗ Referencial oficial SNQ
Índice

Introdução

Esta sebenta ensina a escrever código que escreve código: em vez de repetir a mesma lógica ficheiro após ficheiro, aprendes a generalizar, a gerar e a automatizar. É uma competência central para quem produz conteúdos interativos, porque este trabalho repete-se muito, um quiz atrás de outro, um cartão de produto atrás de outro, uma galeria atrás de outra, e a diferença entre um técnico júnior e um técnico sénior está em saber quando e como automatizar essa repetição em vez de a copiar e colar.

O percurso começa nos princípios abstratos (programação genérica, tipos de linguagens), passa pelas técnicas concretas de geração de código (templates, scaffolding, meta linguagens, IA generativa), consolida com design patterns e metaprogramação, e termina no que torna o código gerado seguro e testável em produção: tipagem dinâmica, frameworks flexíveis, testes em diferentes plataformas e segurança no código gerado. Fecha com as normas de segurança, saúde e ambiente que acompanham qualquer trabalho técnico.

Objetivos de aprendizagem (referencial): criar código com código generativo; adaptar código para automatizar tarefas; desenvolver código com frameworks; testar código em diferentes plataformas; evitando estrangulamentos no processo de geração, garantindo escalabilidade, evitando duplicação excessiva, facilitando a integração e validando entradas para evitar falhas de segurança.

1. Programação genérica

A programação genérica é a técnica de escrever algoritmos e estruturas de dados que funcionam com qualquer tipo, em vez de escrever uma versão diferente para cada tipo de dado.

Porque interessa

Sem programação genérica, escrever uma função ordenar para números obriga a escrever outra quase igual para texto, e outra para objetos. Com genéricos, escreve-se uma vez:

function primeiro<T>(lista: T[]): T {
  return lista[0];
}

primeiro<number>([1, 2, 3]);      // T = number
primeiro<string>(["a", "b"]);     // T = string

O <T> é um parâmetro de tipo: só se define concretamente quando a função é usada. O compilador continua a verificar que o tipo é usado de forma consistente.

Tipos de linguagens e como resolvem a genericidade

Categoria Linguagens Como funciona
Estaticamente tipadas com genéricos TypeScript, Java, C#, Kotlin tipo verificado em compilação, com <T>
Templates (compilação) C++ o compilador gera uma versão do código por cada tipo usado
Dinamicamente tipadas JavaScript, Python, Ruby qualquer valor serve; o tipo só se conhece em execução

Nesta UC o exemplo prático é TypeScript: junta a segurança dos tipos à familiaridade do JavaScript, muito usado na produção de conteúdos interativos para a web.

Exemplo resolvido: um Stack genérico

Precisamos de uma pilha (stack) que funcione tanto para números de pontuação como para nomes de jogadores.

class Pilha<T> {
  private itens: T[] = [];
  empilhar(item: T): void { this.itens.push(item); }
  desempilhar(): T | undefined { return this.itens.pop(); }
}

const pontuacoes = new Pilha<number>();
pontuacoes.empilhar(10);

const nomes = new Pilha<string>();
nomes.empilhar("Ana");

Uma só classe Pilha<T> serve os dois casos. Sem genéricos, seria preciso PilhaDeNumeros e PilhaDeNomes, praticamente iguais.

2. Código generativo e meta linguagens

Código generativo é código que gera outro código, poupando trabalho manual repetitivo e garantindo consistência entre ficheiros parecidos.

Três níveis de código generativo

  1. Geradores clássicos: um script lê dados ou uma especificação e produz ficheiros de código, sem intervenção humana em cada um.
  2. Metaprogramação: código que examina ou modifica código, o seu ou de outro, em tempo de execução ou compilação (ver capítulo 7).
  3. IA generativa de código: assistentes (GitHub Copilot, ChatGPT, Claude) que geram excertos a partir de linguagem natural ou do contexto do ficheiro.

O que é uma meta linguagem

Uma meta linguagem é uma linguagem usada para descrever ou gerar outra linguagem. É o "molde" a partir do qual sai o código final.

Exemplo resolvido: template de texto

Um .hbs (Handlebars) que gera 50 cartões de produto a partir de um único ficheiro de dados:

<div class="cartao">
  <h3>{{nome}}</h3>
  <p>{{preco}} EUR</p>
</div>
const dados = [{ nome: "Cabaz", preco: 12 }, { nome: "Azeite", preco: 8 }];
dados.forEach(item => gerarCartao(template, item));

Um só template + um ficheiro de dados substitui 50 blocos de HTML repetidos e reduz drasticamente o risco de erro de cópia.

3. Ferramentas e técnicas de geração de código

Scaffolding

Scaffolding é gerar a estrutura inicial de um projeto, ou de uma peça de código, em vez de a construir do zero.

Ferramenta O que gera
npm create vite@latest um projeto novo completo, com configuração e exemplo
Plop, Hygen ficheiros a partir de um template e de perguntas ao utilizador
Geradores a partir de schema tipos, validações e até formulários, a partir de uma definição de dados

Assistentes de IA generativa

Ferramentas como GitHub Copilot, ChatGPT ou Claude geram código a partir de um pedido em linguagem natural ou do contexto do ficheiro aberto. Servem para automatizar tarefas repetitivas: escrever um teste previsível, converter um formato de dados, preencher uma função com um padrão já visto no ficheiro.

Não substituem a revisão humana. Um dos critérios de desempenho da UC é evitar a introdução de estrangulamentos no processo de geração, o que aqui significa duas coisas em equilíbrio: não bloquear o trabalho à espera do gerador, mas também não aceitar código gerado sem o ler e testar.

Exemplo resolvido: usar um gerador com Plop

  1. Definir um template de componente interativo (quiz, galeria, video) em plopfile.js.
  2. Correr npx plop componente.
  3. O Plop pergunta o nome do componente e gera automaticamente Quiz.tsx, Quiz.test.tsx e Quiz.module.css, já com a estrutura correta e os nomes preenchidos.

Resultado: cada novo componente segue sempre a mesma estrutura, sem depender da memória de quem o cria.

4. Frameworks e templates de desenvolvimento

Um framework dá a estrutura, as regras e as ferramentas para desenvolver mais depressa. Muitos trazem o seu próprio sistema de templates.

Templates em frameworks de UI

Exemplo resolvido: componente vs cópia manual

Sem template (repetido três vezes, com risco de erro de cópia):

<div class="cartao"><h3>Cabaz</h3><p>12 EUR</p></div>
<div class="cartao"><h3>Azeite</h3><p>8 EUR</p></div>
<div class="cartao"><h3>Mel</h3><p>6 EUR</p></div>

Com um componente React (um template, dados variáveis):

function Cartao({ nome, preco }: { nome: string; preco: number }) {
  return <div className="cartao"><h3>{nome}</h3><p>{preco} EUR</p></div>;
}

produtos.map(p => <Cartao key={p.nome} nome={p.nome} preco={p.preco} />)

Uma alteração ao design do cartão só se faz num sítio, e propaga-se a todos os produtos.

5. Design patterns

Um design pattern é uma solução reutilizável para um problema recorrente no desenho de software. Não é código pronto a copiar, é uma estrutura de raciocínio documentada, com nome próprio, que dá vocabulário comum à equipa técnica.

Princípios por trás dos patterns

Os patterns são a aplicação prática destes princípios: seguir os princípios com disciplina leva, quase sempre, a reinventar um pattern já conhecido.

Tipos de design patterns

Padrões de criação, tratam de como os objetos nascem:

Padrão Ideia
Factory uma função ou classe decide que tipo de objeto criar
Singleton garante uma única instância partilhada
Builder constrói um objeto complexo passo a passo

Padrões estruturais, tratam de como as peças se compõem:

Padrões comportamentais, tratam de como os objetos comunicam:

Exemplo resolvido: Factory de componentes interativos

function criarComponente(tipo: "quiz" | "video" | "galeria"): ComponenteInterativo {
  switch (tipo) {
    case "quiz": return new Quiz();
    case "video": return new Video();
    case "galeria": return new Galeria();
  }
}

const componente = criarComponente("quiz");
componente.render();

O código chamador só conhece a interface comum ComponenteInterativo, não a classe concreta que a Factory devolveu. Se amanhã aparecer um novo tipo ("jogo"), só a Factory muda; o resto do código fica igual.

6. Código genérico: herança e polimorfismo

Herança

A herança permite que uma classe reutilize atributos e métodos de outra, especializando-a.

class ComponenteInterativo {
  render(): void { /* comportamento comum */ }
}

class Quiz extends ComponenteInterativo {
  corrigir(resposta: string): boolean { return true; }
}

Quiz herda render() e acrescenta corrigir(), específico dele. Regra prática: só usar herança quando existe mesmo uma relação "é um" (Quiz é um ComponenteInterativo); caso contrário, prefere-se composição.

Polimorfismo

Polimorfismo é a capacidade de tratar objetos de classes diferentes da mesma forma, através de uma interface comum.

function mostrarTodos(itens: ComponenteInterativo[]) {
  itens.forEach(i => i.render());
}

mostrarTodos([new Quiz(), new Galeria()]);

O código chamador não precisa de saber se cada item é Quiz ou Galeria: chama render() e cada objeto responde de acordo com a sua própria implementação. É o que torna a Factory do capítulo anterior útil na prática: cria objetos diferentes, tratados depois de forma uniforme.

Exemplo resolvido: adicionar um tipo novo sem quebrar nada

Suponha que a Galeria já existe e agora se acrescenta Video:

class Video extends ComponenteInterativo {
  render(): void { /* tocar o vídeo */ }
}

mostrarTodos([new Quiz(), new Galeria(), new Video()]); // funciona sem alterar mostrarTodos

Porque mostrarTodos depende apenas da interface render(), aceitar um novo tipo não exige alterar essa função, apenas criar a nova classe. Isto é o princípio Open/Closed na prática.

7. Metaprogramação

Metaprogramação é escrever código que examina, gera ou modifica outro código, em tempo de execução ou de compilação. É "um nível acima" da programação normal.

Conceitos principais

Frameworks como Angular, NestJS e Spring usam metaprogramação para "ligar as peças" automaticamente, sem que o programador escreva essa ligação à mão.

Exemplo resolvido: um decorator simples

function log(target, key, descriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args) {
    console.log(`A chamar ${key}`, args);
    return original.apply(this, args);
  };
}

class Quiz {
  @log
  corrigir(resposta) { /* lógica de correção */ }
}

O decorator @log envolve corrigir() de fora, sem tocar no seu código interno: acrescenta um registo em consola sempre que corrigir é chamado. É metaprogramação aplicada ao padrão Decorator do capítulo 5.

Proxy e Reflect

O Proxy de JavaScript permite intercetar leituras, escritas e chamadas a um objeto:

const registado = new Proxy({}, {
  set(alvo, propriedade, valor) {
    console.log(`${String(propriedade)} definido para ${valor}`);
    alvo[propriedade] = valor;
    return true;
  }
});

registado.nome = "Ana"; // imprime: nome definido para Ana

O Vue 3 usa Proxy internamente para detetar alterações a dados e atualizar o ecrã automaticamente: é reatividade construída em cima de metaprogramação.

8. Tipagem dinâmica para código genérico

Duck typing

Em linguagens de tipagem dinâmica (JavaScript, Python), o tipo de uma variável só se conhece em execução. Isto dá uma genericidade "natural", conhecida como duck typing: "se anda como pato e faz quack como pato, é um pato", importa o que o objeto sabe fazer, não a classe a que pertence.

function tocar(objeto) {
  objeto.play();
}

tocar(video);  // funciona
tocar(audio);  // também funciona, desde que tenha play()

A vantagem é a flexibilidade: tocar funciona com qualquer objeto que tenha play(). O risco é que erros de tipo só aparecem em execução, por vezes já em produção.

Armazenar e manipular tipos diferentes

Exemplo resolvido: validar antes de usar

function validarIdade(valor: unknown): number {
  const numero = Number(valor);
  if (Number.isNaN(numero) || numero < 0) {
    throw new Error("Idade inválida");
  }
  return numero;
}

Mesmo em JavaScript dinamicamente tipado, validarIdade garante que, a partir daquele ponto, o resto do código trabalha sempre com um número válido. É a disciplina que compensa a flexibilidade da tipagem dinâmica.

9. Frameworks e bibliotecas flexíveis

Arquitetura de frameworks flexíveis

Frameworks e bibliotecas flexíveis oferecem funcionalidades genéricas que se adaptam ao projeto, em vez de imporem uma única forma de trabalhar.

Escolher e integrar bem uma biblioteca

Critério Porque interessa
Manutenção ativa evita depender de código abandonado, com falhas nunca corrigidas
Documentação reduz o tempo de integração e de correção de erros
Tamanho do pacote afeta a velocidade de carregamento do conteúdo interativo
Compatibilidade evita conflitos com outras bibliotecas já usadas no projeto

Fixar versões em package-lock.json garante que o projeto se constrói sempre da mesma forma, hoje e daqui a um ano. O tree-shaking remove do código final o que não é usado da biblioteca, mantendo o resultado mais leve.

Exemplo resolvido: um hook reutilizável

function useContador(inicial: number) {
  const [valor, setValor] = useState(inicial);
  const incrementar = () => setValor(v => v + 1);
  return { valor, incrementar };
}

// reutilizado em qualquer componente:
const { valor, incrementar } = useContador(0);

useContador extrai uma lógica genérica (contar) para ser reutilizada em qualquer componente que precise dela, sem duplicar useState e a função de incremento em cada sítio.

10. Adaptar código para automatizar tarefas

Reconhecer o que vale a pena automatizar

Regra prática: automatiza o que se repete três ou mais vezes. Automatizar uma tarefa única costuma custar mais tempo do que fazê-la à mão.

Adaptar algoritmos a diferentes contextos

Um algoritmo genérico adapta-se ao contexto através de parâmetros e de configuração por dados, sem que a lógica principal mude.

Exemplo resolvido: gerador de certificados reutilizável

Um gerador de certificados de um curso de eletrónica precisa de servir também um curso de programação.

function gerarCertificado(config: { curso: string; horas: number; aluno: string }) {
  return template
    .replace("{{curso}}", config.curso)
    .replace("{{horas}}", String(config.horas))
    .replace("{{aluno}}", config.aluno);
}

gerarCertificado({ curso: "Eletrónica", horas: 25, aluno: "Rui" });
gerarCertificado({ curso: "Programação", horas: 25, aluno: "Ines" });

A função e o template não mudam entre cursos, só os dados de entrada mudam. Isto é automatização bem feita: escrever uma vez, reutilizar sempre que aparece um curso novo.

11. Testar código em diferentes plataformas

Tipos de testes

Tipo Verifica Ferramentas comuns
Unitário uma função ou classe isolada Jest, Vitest
Integração várias peças a funcionar juntas Jest, Vitest
End-to-end (E2E) o utilizador real no browser Playwright, Cypress
De plataforma o mesmo conteúdo em browsers/dispositivos diferentes DevTools, BrowserStack

Testar código gerado é ainda mais importante do que testar código escrito à mão: um erro no gerador propaga-se a todos os ficheiros que ele produz.

Testar em diferentes plataformas na prática

Exemplo resolvido: testar um gerador de componentes

test("gerarCartao produz o nome e preço corretos", () => {
  const html = gerarCartao({ nome: "Cabaz", preco: 12 });
  expect(html).toContain("Cabaz");
  expect(html).toContain("12 EUR");
});

test("gerarCartao escapa HTML no nome", () => {
  const html = gerarCartao({ nome: "<script>", preco: 1 });
  expect(html).not.toContain("<script>");
});

O segundo teste não é só sobre correção funcional, é também um teste de segurança: garante que dados de entrada perigosos não passam diretamente para o HTML gerado.

12. Segurança, normas e boas práticas

Vulnerabilidades comuns em código gerado

Código gerado automaticamente, por templates ou por IA, pode introduzir falhas se não for revisto:

Boas práticas de segurança no código generativo

Segurança e saúde no trabalho

O trabalho de programação é sedentário e exige atenção à ergonomia: ecrã à altura dos olhos, antebraços apoiados, pausas regulares a cada 50-60 minutos para os olhos e para evitar lesões por esforço repetitivo.

Proteção ambiental

Código mais eficiente consome menos processamento e menos energia nos servidores. Green coding significa evitar processamento e armazenamento desnecessários, gerar só o que é preciso e prolongar a vida útil do equipamento sempre que possível.

Erros comuns

Glossário

Síntese

Código generativo é escrever código que escreve código: usa programação genérica para trabalhar com qualquer tipo, templates, scaffolding e IA para gerar o repetitivo, design patterns para dar estrutura e vocabulário comum, e metaprogramação para código que se examina e adapta a si próprio. A tipagem dinâmica dá flexibilidade, mas exige validação disciplinada. Fecha o ciclo em frameworks flexíveis, testes em diferentes plataformas e segurança no código gerado, sempre com as normas de segurança, saúde e ambiente a acompanhar o trabalho técnico.

Exercícios resolvidos

1. Explica em duas frases a diferença entre programação genérica e tipagem dinâmica.

Resolução: a programação genérica usa parâmetros de tipo (<T>) verificados em compilação para funcionar com vários tipos, mantendo a segurança de tipos. A tipagem dinâmica adia essa verificação para a execução, o que dá mais flexibilidade mas também mais risco de erros só aparecerem em produção.

2. Dado o padrão Factory criarComponente(tipo), o que precisas de mudar para acrescentar um novo tipo "jogo"?

Resolução: só a Factory precisa de um novo caso no switch (case "jogo": return new Jogo()) e a nova classe Jogo que implementa a interface ComponenteInterativo. O código que chama mostrarTodos() não muda, porque depende apenas da interface comum, isto é o princípio Open/Closed em ação.

3. Um colega cola um excerto de código gerado por IA diretamente num formulário de contacto, sem o rever. Que dois riscos concretos identificas?

Resolução: risco de injeção (se o código não escapar os dados do utilizador antes de os mostrar ou guardar) e risco de lógica incorreta não detetada (a IA pode ter gerado uma validação incompleta ou um caso limite mal tratado). Ambos só se apanham com revisão e testes.

4. Escreve, em pseudocódigo, um decorator simples que mede o tempo de execução de uma função.

Resolução: js function medirTempo(target, key, descriptor) { const original = descriptor.value; descriptor.value = function (...args) { const inicio = Date.now(); const resultado = original.apply(this, args); console.log(`${key} demorou ${Date.now() - inicio}ms`); return resultado; }; } O decorator envolve a função original, corre um cronómetro antes e depois, e devolve o resultado sem alterar o comportamento da função de origem.

5. Um gerador de páginas de exercícios produz 40 ficheiros. Um teste unitário do gerador falha. Porque é mais grave do que uma página individual estar errada?

Resolução: porque o erro está no gerador, não numa página isolada, o que significa que os 40 ficheiros produzidos têm potencialmente o mesmo erro. Corrigir o gerador e regenerar tudo é mais eficiente e mais seguro do que corrigir cada ficheiro à mão, e o teste unitário evita que o problema chegue a produção.