Sebenta · Desenvolver código genérico para conteúdos digitais (UC04371)
- Introdução
- 1. Programação genérica: conceitos e princípios
- 2. Tipos de linguagens de programação
- 3. Algoritmos genéricos e tipos de dados
- 4. Estruturas de dados genéricas
- 5. Tipagem dinâmica
- 6. Orientação a objetos: classes, herança e polimorfismo
- 7. Design patterns: conceitos, princípios e tipos
- 8. Frameworks e bibliotecas flexíveis
- 9. Adaptar código genérico a diferentes contextos
- 10. Testes de funcionalidades
- 11. Segurança, saúde no trabalho e proteção ambiental
- Erros comuns
- Glossário
- Síntese
- Exercícios resolvidos
Introdução
Esta sebenta ensina a escrever código que serve para mais do que um caso: código genérico. Em vez de uma função só para imagens, outra só para vídeos e outra só para questionários, aprendes a analisar o que os conteúdos digitais têm em comum e a escrever uma única solução que serve a todos, e cresce sem ser reescrita.
O percurso segue a lógica de um profissional de programação: primeiro os conceitos (programação genérica, tipos de linguagens, algoritmos e estruturas de dados), depois a tipagem dinâmica, a orientação a objetos (classes, herança, polimorfismo), os design patterns mais úteis para conteúdos digitais, os frameworks e bibliotecas flexíveis, e por fim como adaptar e testar o código com rigor, sem esquecer as normas de segurança, saúde e ambiente. Os exemplos usam JavaScript, a linguagem mais comum em conteúdos digitais interativos, mas os conceitos são transferíveis para qualquer linguagem.
Objetivos de aprendizagem (referencial): analisar requisitos específicos para conteúdos digitais; criar código genérico para execução de conteúdos digitais; adaptar código genérico; e desenvolver código genérico com frameworks, respeitando os requisitos, evitando complexidade desnecessária e tratando erros de forma genérica.
1. Programação genérica: conceitos e princípios
Código genérico é código escrito para funcionar com vários tipos de dados ou de conteúdos, sem precisar de ser reescrito para cada caso específico. Uma galeria de imagens, um leitor de vídeo e um questionário interativo têm muito em comum: um título, uma forma de aparecer no ecrã, uma forma de ser testado. Código genérico é aquele que trata esse "comum" numa só solução.
Porque programar de forma genérica
- Reutilização: uma função ou classe serve vários casos, sem duplicar lógica.
- Menos duplicação, menos erros: cada correção feita numa única função corrige todos os casos que a usam.
- Facilidade de crescimento: acrescentar um tipo de conteúdo novo não obriga a mexer no que já funciona.
Princípios
- DRY (Don't Repeat Yourself): não escrever a mesma lógica duas vezes.
- Abstração: identificar o que os conteúdos têm em comum, antes de tratar as diferenças.
- Independência de tipo: a função não deve precisar de saber, à partida, que tipo concreto de conteúdo vai receber.
- Baixo acoplamento: módulos que dependem pouco uns dos outros são mais fáceis de adaptar e de testar.
⚠️ Genérico não é sinónimo de complicado. Um dos critérios de desempenho da UC é precisamente evitar complexidades desnecessárias: generalizar só quando há, de facto, mais do que um caso a servir.
2. Tipos de linguagens de programação
Um paradigma de programação é uma forma de organizar o raciocínio ao escrever código. Conhecer os principais ajuda a escolher a ferramenta certa para cada problema.
| Paradigma | Ideia central | Exemplo de linguagem |
|---|---|---|
| Imperativo | sequência de instruções que mudam o estado do programa | C, JavaScript |
| Orientado a objetos | dados e comportamento agrupados em objetos | Java, C#, JavaScript |
| Funcional | funções puras, sem efeitos secundários | Haskell, partes de JavaScript |
| Declarativo | descreve o resultado desejado, não os passos | SQL, HTML/CSS |
A maioria das linguagens modernas, incluindo o JavaScript, é multi-paradigma: combina vários estilos conforme o que resolve melhor cada problema.
Compiladas vs interpretadas, estáticas vs dinâmicas
- Linguagens compiladas: o código é traduzido para linguagem de máquina antes de correr (C, Java parcialmente, via bytecode).
- Linguagens interpretadas: o código é lido e executado diretamente, linha a linha, em tempo de execução (JavaScript, Python).
- Tipagem estática: o tipo de cada variável é fixado e verificado antes de o programa correr (Java, TypeScript).
- Tipagem dinâmica: o tipo é determinado durante a execução e pode mudar (JavaScript, Python).
Para conteúdos digitais na web, a combinação típica é interpretada e de tipagem dinâmica: o JavaScript, que corre diretamente no browser, sem instalação nem compilação prévia por parte de quem visita a página.
3. Algoritmos genéricos e tipos de dados
Um algoritmo é uma sequência de passos, bem definida, que resolve um problema. Um algoritmo genérico resolve o problema de forma independente do tipo concreto de dado: por exemplo, um algoritmo de ordenação que funciona tanto para números como para nomes de conteúdos.
Exemplo resolvido: uma função de ordenação genérica
function ordenar(lista, comparar) {
return [...lista].sort(comparar);
}
const numeros = [5, 2, 9, 1];
const ordenadosAsc = ordenar(numeros, (a, b) => a - b);
// [1, 2, 5, 9]
const titulos = ["Zebra", "Abelha", "Morcego"];
const ordenadosAbc = ordenar(titulos, (a, b) => a.localeCompare(b));
// ["Abelha", "Morcego", "Zebra"]
Passo a passo:
1. A função ordenar recebe qualquer lista e uma regra de comparação.
2. Não sabe, à partida, se os elementos são números ou texto: quem chama a função decide isso através do parâmetro comparar.
3. O mesmo algoritmo (usar sort com uma regra) serve os dois casos, sem duplicar código.
Tipos de dados
- Primitivos: número, texto (string), booleano,
nulleundefined. - Compostos: array (lista ordenada), objeto (pares chave-valor), e estruturas mais avançadas como conjuntos e mapas.
Um algoritmo genérico costuma operar sobre dados compostos, aplicando a mesma lógica de forma consistente a cada elemento da coleção.
4. Estruturas de dados genéricas
A estrutura de dados certa é o alicerce de um algoritmo genérico eficiente. Cada estrutura guarda os dados de uma forma diferente e serve melhor um tipo de problema.
| Estrutura | O que guarda | Uso típico em conteúdos digitais |
|---|---|---|
| Array / lista | elementos ordenados, acedidos por posição | lista de slides, galeria de imagens |
| Objeto / dicionário | pares chave-valor | as propriedades de um conteúdo (título, tipo, url) |
| Conjunto (Set) | valores únicos, sem repetição | as tags de um curso, sem duplicados |
| Fila (Queue) | primeiro a entrar, primeiro a sair (FIFO) | fila de reprodução de vídeos |
| Pilha (Stack) | último a entrar, primeiro a sair (LIFO) | histórico de "recuar" num quiz |
Todas são genéricas: guardam qualquer tipo de conteúdo, desde que se respeite a mesma forma de organização.
Exemplo resolvido: gerir uma coleção de conteúdos
const conteudos = [
{ id: 1, tipo: "imagem", titulo: "Capa" },
{ id: 2, tipo: "video", titulo: "Introdução" },
{ id: 3, tipo: "quiz", titulo: "Autoavaliação" },
];
const tipos = new Set(conteudos.map(c => c.tipo));
// Set { "imagem", "video", "quiz" }
const primeiro = conteudos[0]; // acesso por posição, típico de array
const porId = conteudos.find(c => c.id === 2); // procurar por chave
O array guarda a coleção ordenada; cada objeto guarda as propriedades de um conteúdo de forma genérica (o mesmo formato serve uma imagem, um vídeo ou um quiz); o Set dá, sem código extra, a lista de tipos sem repetições.
5. Tipagem dinâmica
Em tipagem dinâmica, o tipo de uma variável não é declarado à partida: é determinado, em cada momento, pelo valor que ela guarda.
let valor = 10; // é um número
valor = "dez"; // agora é texto
valor = [1, 2, 3]; // agora é um array
Vantagens e riscos
- Vantagem: flexibilidade e código mais curto, ideal para funções genéricas que aceitam vários tipos de conteúdo.
- Risco: erros que só aparecem em execução, por exemplo somar um número com um texto sem querer.
Boas práticas com tipagem dinâmica
- Verificar o tipo com
typeofantes de operar sobre um valor incerto. - Usar valores por omissão para evitar comportamento inesperado com
undefined. - Documentar, em comentário, que tipo cada função espera receber.
Exemplo resolvido: verificar o tipo antes de usar
function mostrarConteudo(item) {
if (typeof item !== "object" || item === null) {
throw new Error("mostrarConteudo espera um objeto de conteúdo");
}
console.log(`${item.tipo}: ${item.titulo}`);
}
mostrarConteudo({ tipo: "video", titulo: "Aula 1" }); // ok
mostrarConteudo("texto"); // lança erro, de forma controlada
A tipagem dinâmica dá liberdade, mas o código genérico bem escrito verifica o que recebe, em vez de assumir.
6. Orientação a objetos: classes, herança e polimorfismo
A orientação a objetos organiza o código em torno de classes (moldes) e objetos (instâncias concretas), e é a base mais comum para escrever código genérico com herança e polimorfismo.
Classes e encapsulamento
class ConteudoDigital {
constructor(titulo, duracaoSeg = 0) {
this.titulo = titulo;
this.duracaoSeg = duracaoSeg;
}
render() {
return `Conteúdo genérico: ${this.titulo}`;
}
}
const c1 = new ConteudoDigital("Capa do curso");
O encapsulamento junta os dados (titulo, duracaoSeg) e o comportamento (render) na mesma classe. Cada new ConteudoDigital(...) cria um objeto: uma instância concreta, com os seus próprios valores.
Herança: reaproveitar código genérico
A herança cria uma classe nova a partir de outra, reaproveitando o que já existe:
class Video extends ConteudoDigital {
constructor(titulo, duracaoSeg) {
super(titulo, duracaoSeg);
}
render() {
return `A reproduzir "${this.titulo}" (${this.duracaoSeg}s)`;
}
}
const aula = new Video("Aula 1", 300);
extendsligaVideoaConteudoDigital, a classe-mãe.super(...)chama o construtor da classe-mãe, para não repetir a lógica de inicialização.Videosobrepõe (override) o métodorender(), mas podia ter herdado outros métodos sem os reescrever.
Polimorfismo: a mesma chamada, comportamentos diferentes
Polimorfismo é a capacidade de objetos de classes diferentes responderem à mesma chamada de método cada um ao seu modo.
class Quiz extends ConteudoDigital {
constructor(titulo, perguntas) {
super(titulo);
this.perguntas = perguntas;
}
render() {
return `Quiz "${this.titulo}" com ${this.perguntas.length} pergunta(s)`;
}
}
const conteudos = [
new Video("Aula 1", 300),
new Quiz("Autoavaliação", ["P1", "P2"]),
new ConteudoDigital("Capa"),
];
for (const item of conteudos) {
console.log(item.render());
}
// A reproduzir "Aula 1" (300s)
// Quiz "Autoavaliação" com 2 pergunta(s)
// Conteúdo genérico: Capa
O ciclo for não sabe se cada item é um vídeo, um quiz ou um conteúdo genérico: chama sempre render(), e cada objeto responde à sua maneira. É isto que torna o código genérico: se amanhã criarmos uma classe Audio, este ciclo continua a funcionar sem qualquer alteração.
7. Design patterns: conceitos, princípios e tipos
Um design pattern (padrão de projeto) é uma solução testada para um problema que se repete no desenho de software, com um nome e um vocabulário partilhados pelos programadores. Não é código pronto a copiar: é uma estrutura de raciocínio a adaptar a cada caso.
As três categorias clássicas
| Categoria | Resolve | Exemplos |
|---|---|---|
| Criação | como instanciar objetos | Factory, Singleton |
| Estrutura | como organizar classes e objetos | Adapter, Decorator |
| Comportamento | como os objetos comunicam entre si | Strategy, Observer |
Factory: centralizar a criação
O Factory concentra a criação de objetos numa única função, que decide qual classe instanciar a partir de um parâmetro:
function criarConteudo(tipo, titulo) {
switch (tipo) {
case "imagem": return new Imagem(titulo);
case "video": return new Video(titulo);
case "quiz": return new Quiz(titulo);
default: throw new Error(`Tipo desconhecido: ${tipo}`);
}
}
O resto do programa nunca escreve new Video(...) diretamente: pede sempre à factory. Se a forma de construir um Video mudar, só é preciso alterar um sítio.
Strategy: um algoritmo intercambiável
O Strategy encapsula um algoritmo que pode ser trocado, passado como parâmetro. A função ordenar do capítulo 3 já é um exemplo de Strategy, mesmo sem lhe termos dado o nome:
const estrategiaAlfabetica = (a, b) => a.titulo.localeCompare(b.titulo);
conteudos.sort(estrategiaAlfabetica);
Observer: notificar sem conhecer em detalhe
O Observer permite que um objeto (o sujeito) notifique vários observadores quando algo acontece, sem os conhecer em detalhe:
document.addEventListener("conteudoTerminado", () => {
console.log("Avançar para o próximo conteúdo");
});
Um leitor de vídeo pode disparar o evento conteudoTerminado, e vários módulos independentes (estatísticas, navegação, notas) reagirem, cada um à sua maneira, sem o leitor de vídeo saber que módulos existem.
8. Frameworks e bibliotecas flexíveis
- Biblioteca: um conjunto de funções que o nosso código chama quando precisa. Nós controlamos o fluxo do programa.
- Framework: uma estrutura que chama o nosso código em pontos definidos. O framework controla o fluxo, um princípio chamado inversão de controlo.
| Biblioteca | Framework | |
|---|---|---|
| Quem controla o fluxo | o programador | o framework |
| Exemplo | uma função de datas | React, Vue |
| Flexibilidade | maior | segue convenções próprias |
Funcionalidades genéricas, arquitetura e extensibilidade
Um framework ou biblioteca flexível para conteúdos digitais costuma oferecer:
- Funcionalidades genéricas: carregar, mostrar e remover qualquer tipo de conteúdo, sem código específico por tipo.
- Arquitetura em camadas: separação entre dados, lógica e apresentação.
- Extensibilidade: pontos de extensão (plugins, hooks) que permitem acrescentar tipos de conteúdo novos sem alterar o núcleo do sistema.
const registoDeTipos = {};
function registarTipoDeConteudo(nome, Classe) {
registoDeTipos[nome] = Classe;
}
registarTipoDeConteudo("audio", Audio); // extensão sem tocar no núcleo
Um bom framework cresce com o projeto, sem obrigar a reescrever o que já funciona: é a extensibilidade a funcionar na prática.
9. Adaptar código genérico a diferentes contextos
Adaptar código genérico é configurá-lo, não reescrevê-lo por dentro. As práticas mais comuns:
- Parâmetros e opções: passar valores para alterar o comportamento, por exemplo
{ autoplay: true }. - Injeção de dependências: passar a função ou o objeto certo à função, em vez de o fixar dentro dela.
- Configuração externa: um ficheiro de definições em vez de valores fixos no código (hardcoding).
- Composição: combinar peças pequenas e genéricas em vez de escrever uma peça grande e específica.
⚠️ Erro comum: fixar um valor "à mão" no meio de uma função (hardcoding), em vez de o receber como parâmetro. Torna o código difícil de reutilizar.
Exemplo resolvido: adaptar a biblioteca a um novo contexto
A mesma classe ConteudoDigital do capítulo 6, adaptada a um contexto totalmente diferente, um museu virtual em vez de um curso:
class Exponente extends ConteudoDigital {
constructor(titulo, sala) {
super(titulo);
this.sala = sala;
}
render() {
return `Exponente "${this.titulo}" na sala ${this.sala}`;
}
}
const peca = new Exponente("Jarrão romano", "Sala 3");
peca.render(); // Exponente "Jarrão romano" na sala Sala 3
Nada mudou na classe base ConteudoDigital, nem no ciclo for que percorre conteúdos: só se acrescentou uma subclasse nova. É esta a prova de que o código está, de facto, bem generalizado.
10. Testes de funcionalidades
Testar é verificar, de forma sistemática, que o código faz o que devia fazer, incluindo em casos-limite.
- Teste manual: correr o programa e observar o resultado.
- Teste automatizado: escrever código que verifica outro código, mais fiável e repetível.
- Casos-limite: valores vazios,
null, tipos inesperados, especialmente relevantes com tipagem dinâmica.
Exemplo resolvido: testar um método herdado
class ConteudoDigital {
constructor(titulo) { this.titulo = titulo; }
testar() {
return typeof this.titulo === "string" && this.titulo.length > 0;
}
}
console.assert(new ConteudoDigital("Aula 1").testar() === true,
"Título válido devia passar");
console.assert(new ConteudoDigital("").testar() === false,
"Título vazio devia falhar");
Como testar() está na classe base, todas as subclasses (Video, Quiz, Imagem) herdam o mesmo teste, sem código extra.
Tratar erros de forma genérica
Uma função genérica deve prever dados inesperados e reagir de forma consistente:
function renderSeguro(item) {
try {
return item.render();
} catch (erro) {
console.error(`Falha ao renderizar: ${erro.message}`);
return "Conteúdo indisponível";
}
}
O bloco try/catch isola o erro sem parar o resto do programa: é o critério de desempenho "tratando e corrigindo erros de forma genérica" a funcionar na prática.
11. Segurança, saúde no trabalho e proteção ambiental
Escrever código também tem uma dimensão de segurança, saúde e ambiente, parte do referencial desta UC.
Normas de segurança e saúde no trabalho
- Postura: ecrã à altura dos olhos, costas direitas, pés apoiados.
- Pausas regulares: a cada 50 a 60 minutos, levantar e descansar. A regra 20-20-20: a cada 20 minutos, olhar 20 segundos para algo a cerca de 6 metros de distância.
- Ergonomia do posto: iluminação adequada, sem reflexos no ecrã; cadeira e secretária ajustadas.
- Organização do posto de trabalho: cabos arrumados, para evitar quedas e riscos elétricos.
Normas de proteção ambiental
- Eficiência do código: código mais eficiente consome menos energia no servidor e no dispositivo de quem o usa.
- Otimizar recursos: imagens e vídeos bem dimensionados reduzem tráfego e consumo energético.
- Ciclo de vida do equipamento: prolongar a vida útil do equipamento informático e reciclar corretamente o que é descartado.
- Consumo consciente: desligar equipamento não utilizado e preferir modos de poupança de energia.
Rigor no código também é uma forma de responsabilidade ambiental, uma das atitudes valorizadas no referencial desta UC.
Erros comuns
- Duplicar funções por tipo de conteúdo (
mostrarImagem,mostrarVideo) em vez de generalizar. - Generalizar cedo demais, criando abstrações para casos que nunca chegam a acontecer.
- Esquecer o
super(...)no construtor de uma subclasse, e o construtor falhar. - Verificar o tipo com
if/elseem vez de deixar o polimorfismo tratar do comportamento certo. - Fixar valores no código (hardcoding) em vez de os receber como parâmetro.
- Só testar o caminho feliz, esquecendo valores vazios,
nullou tipos inesperados. - Deixar um
catchvazio, escondendo o erro em vez de o tratar.
Glossário
- Código genérico: código escrito para funcionar com vários tipos de dados ou de conteúdos.
- DRY (Don't Repeat Yourself): princípio de não repetir a mesma lógica.
- Paradigma: forma de organizar o raciocínio ao programar (imperativo, orientado a objetos, funcional, declarativo).
- Tipagem dinâmica: o tipo de uma variável é determinado em tempo de execução e pode mudar.
- Algoritmo genérico: sequência de passos independente do tipo concreto de dado.
- Estrutura de dados: forma de organizar e guardar dados (array, objeto, conjunto, fila, pilha).
- Classe: molde que descreve propriedades e comportamento comuns a um conjunto de objetos.
- Herança: mecanismo que permite criar uma classe a partir de outra, reaproveitando o que já existe.
- Polimorfismo: capacidade de objetos de classes diferentes responderem à mesma chamada, cada um ao seu modo.
- Design pattern: solução testada para um problema recorrente no desenho de software.
- Framework: estrutura que chama o código do programador em pontos definidos (inversão de controlo).
- Biblioteca: conjunto de funções chamadas pelo código do programador, conforme a necessidade.
- Injeção de dependências: prática de passar o objeto ou a função necessária a uma função, em vez de a fixar por dentro.
- Hardcoding: fixar um valor diretamente no código, em vez de o tornar configurável.
Síntese
Código genérico para conteúdos digitais nasce de abstrair o que é comum: um algoritmo, uma estrutura de dados, uma classe base. A tipagem dinâmica do JavaScript dá flexibilidade, e a orientação a objetos, com herança e polimorfismo, permite que uma base única sirva imagens, vídeos, questionários ou qualquer outro conteúdo futuro. Os design patterns (Factory, Strategy, Observer) e os frameworks flexíveis dão vocabulário e estrutura a este trabalho. Adaptar é configurar, não reescrever; e nenhum código genérico está pronto sem testes que cubram os casos-limite e um tratamento de erros consistente. Tudo isto, sem esquecer as normas de segurança, saúde e ambiente do posto de trabalho.
Exercícios resolvidos
1. Porque é que a função ordenar(lista, comparar) do capítulo 3 é considerada código genérico?
Resolução: porque não depende do tipo concreto dos elementos da lista (números, texto, objetos): recebe a regra de comparação como parâmetro, em vez de a fixar dentro da função. A mesma função serve qualquer tipo de dado que se saiba comparar.
2. Uma turma tem duas funções, mostrarImagem(img) e mostrarVideo(vid), quase iguais. Como resolverias isto com herança e polimorfismo?
Resolução: criar uma classe base
ConteudoDigitalcom um métodorender(), e duas subclassesImagemeVideoque sobrepõemrender()com o comportamento próprio. Um único cicloforchamaitem.render()para qualquer uma das duas, semif/elsea verificar o tipo.
3. Que design pattern está a ser usado neste código, e para quê?
function criarConteudo(tipo, titulo) {
if (tipo === "video") return new Video(titulo);
if (tipo === "quiz") return new Quiz(titulo);
}
Resolução: o padrão Factory. Centraliza a criação de objetos numa única função, para que o resto do programa nunca precise de escrever
new Video(...)diretamente, facilitando alterações futuras à forma de construir cada tipo.
4. Distingue framework de biblioteca com um critério claro.
Resolução: o critério é quem controla o fluxo. Numa biblioteca, o programador chama as funções quando precisa. Num framework, é o framework que chama o código do programador, em pontos definidos, o princípio da inversão de controlo.
5. Um colega escreve catch (erro) {} sem fazer mais nada. Explica porque é um erro e como corrigir.
Resolução: um
catchvazio esconde o erro, sem o registar nem o tratar, tornando o programa difícil de depurar. Devia, no mínimo, registar o erro (console.error) e devolver um resultado seguro, como no exemplorenderSegurodo capítulo 10.
6. Porque é que a tipagem dinâmica exige mais cuidado ao escrever código genérico?
Resolução: porque o tipo de uma variável só é conhecido em tempo de execução e pode mudar, o que significa que uma função pode receber um valor de tipo inesperado. Por isso o código genérico deve verificar o tipo com
typeofantes de operar, em vez de assumir que recebeu o tipo certo.