Repensando o framework (a volta do caseiro)
O que é um framework? Qual a diferença entre um framework e uma biblioteca? Conseguimos viver sem frameworks? Vale à pena desenvolver um framework caseiro?
Há termos que de tão usados acabam perdendo seu significado: dentre estes um que me impressiona é "framework". Parece um assunto já resolvido, mas eu diria que sim, foi resolvido mas deixou de ser nos últimos anos.
Prova disto são posts "recentes", como este (Why You Shouldn't Use a Web Framework), este outro (Don't Use Frameworks), este aqui (Web Frameworks: Why You Don't Always Need Them). Sabe o que é interessante nestes três posts? Não falam o que é um framework, dizem que você deve evitá-los e, ao final, acabam propondo que você implemente o seu próprio... framework.
Além disto há também a consagração do uso de Go como alternativa para o desenvolvimento backend para a web, mostrando frameworks que são, em sua maior parte essencialmente iguais ou agregando muito pouco ao que a biblioteca padrão da linguagem já nos oferece (este vídeo é ótimo). Observe que neste parágrafo você pode me criticar usando minha frase dita no parágrafo anterior.
Chegou então o momento de relembrarmos o que é um framework pra entender o que são e, o mais importante na minha opinião: você não consegue fugir dele.
Mas o que é um framework?
Meu primeiro contato com o termo “framework” foi quando comecei a aprender Java. Naquela época (1996, 97, bem mais em 2001, 2002) muito se falava a respeito de frameworks, especialmente após o lançamento do Struts. Confesso que não conseguia entender direito o significado do termo, e não é pra menos. Vejam o que me diziam na época a respeito:
- “Framework é uma aplicação pré-pronta”.
- “É um conjunto de códigos que você pode reaproveitar em seus projetos e que te poupa muito tempo”.
- “É uma solução pré-pronta para problemas recorrentes”.
Mas e uma “biblioteca”? Ela também não é um conjunto de códigos pré-prontos que eu posso reutilizar?
Se existem estas duas palavras: biblioteca e framework, seriam estas sinônimos? Se sim, por que veio depois este termo, framework? Será que eu o descobri posteriormente e que o mesmo sempre existiu (sim)? Já adianto: não são sinônimos e é fundamental entender esta diferença.
O que é uma biblioteca?
Esta sim é um conjunto de códigos reutilizáveis que vão se apresentar sob a forma de funções, classes, módulos…
Seu código invoca sua execução e como consequência o trabalho pesado é realizado para você. Vamos a alguns exemplos práticos. O código a seguir, escrito em Java (extraído daqui) usa as bibliotecas de compressão da linguagem para gerar um arquivo zip.
package com.mkyong.zip;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class App
{
public static void main( String[] args )
{
byte[] buffer = new byte[1024];
try{
FileOutputStream fos = new FileOutputStream("C:\\MyFile.zip");
ZipOutputStream zos = new ZipOutputStream(fos);
ZipEntry ze= new ZipEntry("spy.log");
zos.putNextEntry(ze);
FileInputStream in = new FileInputStream("C:\\spy.log");
int len;
while ((len = in.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
in.close();
zos.closeEntry();
//remember close it
zos.close();
System.out.println("Done");
}catch(IOException ex){
ex.printStackTrace();
}
}
}
FileOutputStream, ZipOutputStream, ZipEntry, FIleInputStream são classes disponibilizadas pela biblioteca padrão do Java SE. Observe algo interessante: eu simplesmente as importo e, na sequência, meu código define quando estas serão executadas e como.
É possível criar novas sub-classes baseadas nestas que acabei de mencionar, entretanto alguns aspectos se mantém:
- Estou usando código pré-existente que não foi escrito por mim.
- Meu código está definindo quando usarei estas classes/funções.
Outro exemplo bem simples podemos ver no código C escrito a seguir:
#include <stdio.h>
int main(void) {
printf("Aqui de buenas usando a função printf");
return 0;
}
Agora ao invés de classes estou importando a biblioteca de I/O clássica da linguagem C e, na sequência, reaproveito a função printf, disponibilizada por esta. Mais uma vez o fluxo do programa é definido inteiramente por mim: imprima este texto inútil e na sequência retorne 0.
Então vamos à definição de uma biblioteca: código escrito por terceiros focando o reuso e cuja ordem de execução é definida inteiramente por quem o invoca.
E o framework, o que é?
É uma aplicação semi pronta? Sim, mas esta é uma definição vazia (bibliotecas também podem ser consideradas como tal). É um conjunto de código que posso reaproveitar? Óbvio, mas também é uma descrição incompleta e ainda gera confusão em relação ao termo biblioteca. É uma solução para um problema recorrente? Também. Então, qual a diferença?
O significado fica mais claro quando pensamos na tradução do termo para o português: framework pode ser traduzido como moldura, armação. Imagens surgem em minha mente:
Armação, vigamento e moldura: o que tem em comum?
A moldura existe para suportar a gravura/imagem, o vigamento para que as telhas ou piso possam ser incluídos acima de si e a armação dos óculos fornece a estrutura necessária para que as lentes possam estar bem posicionadas em relação à nossa face. Observe que nos três casos servem para posicionar algo. E o framework?
Enquanto no caso da biblioteca o ciclo de vida do nosso código é de nossa responsabilidade, ao lidamos com o framework temos o contrário: nós fornecemos a gravura para a moldura, o piso para o vigamento e as lentes para a armação.
Não somos nós que definimos quando nosso código é invocado: o responsável é o framework. Se você já trabalhou com Spring ou Angular talvez isto lhe soe familiar. Lá vai: frameworks nada mais são que a aplicação mais básica do conceito de inversão de controle. A propósito, o Martin Fowler tem um texto muito bom sobre isto.
Vamos a um exemplo bem simples baseado em Angular? Imagine que eu queira escrever um componente: então escrevo algo similar ao código a seguir usando TypeScript:
@Component({
selector: 'login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
credentials: Credentials;
constructor(private usuarioService: UsuarioService) {
this.credentials = new Credentials(null,null);
}
ngOnInit() {
}
entrar() {
this.usuarioService.auth(this.credentials);
}
}
Aquela função ali, ngOnInit, não sou eu quem a invoco: é o Angular. Nosso papel aqui é fornecer os componentes que serão manipulados pelo framework. Eu sei que aquele método será invocado, mas não por mim. Em momento algum escrevo código como o exemplo a seguir:
usuarioService = new UsuarioService();
login = new LoginComponent(usuarioService);
login.ngOnInit();
O código acima é aquele que tipicamente escrevemos ao lidar com bibliotecas. Mas o que ganhamos com esta delegação? Será que minha função realmente será chamada?
Lembra quando mencionei que sim, frameworks são soluções para problemas que ocorrem com frequência e que também são aplicações pré-prontas, mas que vão além? O problema que todo framework resolve é essencialmente o ciclo de vida da execução do nosso código.
Autores de frameworks se preocupam com a vida de nossos objetos/funções: quando devem ser criados, usados e destruídos. E isto de uma forma ótima, garantindo que nosso trabalho consista apenas em fornecer o conteúdo necessário para que nossas necessidades de negócio sejam atendidas.
Vamos a mais um exemplo: desta vez usando a API Servlet do Java. Um Servlet pode ter diversas formas, a mais comum é o HTTP que tem como objetivo receber requisições pelo protocolo (HTTP), executar nossas regras e, finalmente, retornar conteúdo ao usuário final da aplicação. Vamos implementar um servlet?
package br.com.kicosoft;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ServletDemo1 extends HttpServlet{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException{
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<body>");
out.println("
<h1>Sou um servlet feliz!</h1>
");
out.println("</body>");
out.println("</html>");
}
}
Primeiro eu escrevo a minha classe, que extenderá uma outra chamada HttpServlet. Meu papel consiste apenas em sobrescrever os métodos HTTP que me interessam, no caso, doGet, que corresponde ao método GET do protocolo. Minha função define o que deverá ser retornado ao usuário: o conteúdo HTML que você vê em meu código.
Repare na riqueza: não preciso me preocupar em implementar o protocolo HTTP, ou mesmo um algoritmo de espera (ocupada ou não) de requisições que chegam ao meu servidor. Apenas forneço ao meu framework o código que desejo ser executado.
Então, como posso definir um framework à moda Kiconiana? É o maestro responsável por orquestrar seu código visando resolver um ou mais problemas específicos .
O problema específico pode ser a construção de uma aplicação web, uma integração (pense em Apache Camel implementando os Enterprise Integration Patterns), um jogo (pense em um engine como o Unity). Seu papel é portanto apenas fornecer o código que será executado pelo framework. O que nos leva à grande questão.
Frameworks são burocráticos e isto é bom
Na nossa experiência com manutenção de legados uma das principais causas de problemas é justamente se esquecer deste fato: nos frameworks há convenções que devem ser seguidas. Resumindo: frameworks não são feitos para serem usados como bibliotecas.
O termo burocracia tem uma conotação bastante negativa mas é um fato necessário para que haja ordem, para que se consiga formalizar a identificação de elementos.
(Sabia que a escrita nasce de uma necessidade burocrática? Dica: pesquise sobre a origem da escrita cuneiforme e os sumérios.
Lembre: burocracia é uma coisa, burrocracia, outra.)
Imagine que você irá escrever um framework: seu papel é orquestrar a execução de código de terceiros, certo? Como você o identifica este código e o papel a ser desempenhado por este? Alguma formalização precisa ser posta em prática, o que ocorrerá através da definição de padrões, tais como:
- Presença de arquivos de configuração que identifique os artefatos. Lembra do XML no Java ou Spring?
- Inclusão de anotações em nosso código. Olha ali o meu exemplo usando Angular e a anotação @Component.
- A implementação de uma interface ou extensão de classes (lembra do meu Servlet?).
- Padronizações no código como, por exemplo, a definição de nomes padrão para eventos de ciclo de vida dos objetos (já viu o finado Tapestry?).
- Qualquer outro dispositivo que sirva para marcar o papel desempenhado pelo código a ser orquestrado.
O principal problema que encontro em código legado é justamente este: quem o evoluiu não se esforçou o suficiente para conhecer o framework adotado na escrita do projeto. E ainda pior: usou esta “moldura” como se fosse uma biblioteca, desrespeitando completamente o ciclo de vida proposto por esta.
Sabe aquele sujeito que sempre “dá um jeitinho” na burocracia cortando caminho? É o programador que usa o framework como biblioteca e ferra a gente no futuro.
Frameworks são limitados e isto também é bom
Assim como o termo burocracia, a palavra limitada também tem uma conotação negativa, mas infelizmente é verdade: frameworks são limitados (e tem de ser). Aqui nasce outro problema que encontro em muito código legado.
Todo framework, por mais que busque ser a coisa mais genérica possível (veja o Spring core, por exemplo) sempre busca resolver um contexto específico de problemas. Spring Boot, por exemplo, visa a implementação de backend web (inicialmente era mais focado em micro serviços, mas hoje esta história mudou). Apache Camel você usa para integrações. Passport para gerenciar autenticação, e por aí vai.
O principal erro é pensar no framework como algo genérico: você se pega usando faca como chave de fenda e o resultado sempre é triste.
O que a armação, a moldura e o vigamento tem em comum? Eles limitam a forma do que irão orquestrar.
Frameworks de terceiros e internos ou... você não consegue ficar SEM um framework
Este tem sido, na minha nada humilde opinião, a principal causa de confusões conceituais. Aliás, todos os textos que menciono no início deste post ignoram isto.
Uma coisa são frameworks implementados por terceiros: Spring, Camel, Gin, Express, Angular, Vue... Outra são frameworks implementados internamente por sua equipe.
Observe que normalmente as críticas a frameworks está voltada à sua complexidade. Complexidade esta que é excessiva em relação ao CONTEXTO de um projeto. Problema resolvido: você não usa aquele framework.
Você opta por então não usar framework algum: diz pra todo mundo que não usa frameworks. E escreve código em Go, como este:
package main
import (
"net/http"
"os"
)
func main() {
http.HandleFunc("/shopify/sale", shopify.ProcessPaymentRequest)
http.HandleFunc("/health", health.HealthCheckEndpoint)
var port = data.GetConfig("PORT", "8080")
log.Printf("Iniciando Async na porta " + port)
http.ListenAndServe(":"+port, nil)
}
Tá vendo aquelas linhas "http.HandleFunc" usando a biblioteca padrão do Go? Então: elas que definem quando suas funções vão ser invocadas, né? Adivinha só: são um framework.
Na prática conforme seu projeto aumenta de tamanho em algum momento será necessário definir convenções e padrões a serem seguidos. E é neste momento que ele aparece: o framework caseiro.
A volta do framework caseiro
É interessante vermos a volta da questão do framework caseiro. Como estratégia de arquitetura corporativa, pode (PODE) ser uma escolha muito interessante, mas questões precisam ser levantadas:
- Há uma equipe engajada na evolução deste framework?
- O escopo está definido ao ponto de ser fácil a evolução deste projeto?
- O que o framework caseiro oferece é MELHOR que aquilo oferecido pelo mercado por terceiros?
- Existe bem definidos processos de atualização de aplicações quando novas versões do framework forem lançadas?
- Como lidar com quebras de compatibilidade?
- Daremos conta de manter este framework atualizado com as tendências de mercado?
- O público deste framework é apenas interno?
- É interessante abrir este código fonte (se for possível abrir)?
- Minha equipe conseguirá aplicar com sucesso o framework caseiro em nossos projetos? A documentação é boa o suficiente para que aqueles que não conhecem o código fonte do framework saibam aplicá-lo?
A principal justificativa que escuto relativa a estes frameworks é o domínio absoluto sobre seu funcionamento. De fato, é uma vantagem real. Mas a questão é: você e sua equipe realmente dominam mais as entranhas deste funcionamento que as equipes 100% dedicadas à implementação de frameworks terceirizados?
Esta equipe tem de ir além da implementação: precisa gerar documentação de qualidade, oferecer suporte aos usuários finais, planejar a evolução futura.
Normalmente o que observamos é a mesma equipe lidar tanto com o framework quanto com os projetos nos quais ele é aplicado.
Concluindo
A questão do framework caseiro voltou, e apesar das perguntas que levantei no tópico anterior serem pesadas, ele está cada vez mais popular.
Já vimos o mesmo ocorrer em Java e PHP, por exemplo. Em ambos os casos frameworks terceirizados eclipsaram os framworks caseiros e pra maior parte das pessoas isto foi ótimo.
Vemos agora algo similar em Go, Rust e desenvolvimento web.
É importante ter claras algumas armadilhas ao escrever seu código:
- Saber se precisa realmente de um framework ou de uma biblioteca (em 95% das vezes usará os dois no mesmo projeto).
- Saber como usar uma biblioteca e, principalmente, um framework.
- Buscar entender as formalizações definidas pelos autores do seu framework (poderíamos falar o mesmo a respeito de bibliotecas: pense em OpenGL).
- Entender para qual fim o framework foi escrito e usá-lo apenas para este fim.
- Resumindo o resumo: você deve respeitar o framework.
- Vou dar conta de manter um framework caseiro? Caso não seja o caso, qual caminho seguir na migração para outra alternativa? Reescrita é uma opção?