É inegável que os frameworks são excelentes, eles nos ajudam a ser mais produtivos e entregarmos melhores sistemas já que pegamos emprestado anos de expertise.
Mas sempre devemos usar um framework?
Eu sempre acredito que não existe verdade absoluta nem bala de prata. Apesar de usar muito frameworks no meu dia-a-dia e de acreditar que na grande maioria das vezes o uso faz sentido, neste post gostaria de "provocar"
o pensamento no sentido contrário.
Tópicos
É raro, mas acontece muito!
Para quem não sabe onde quer ir, qualquer lugar serve
Dissecando a primeira solução
Entendendo outros aspectos além do código
Vamos colocar mais pimenta nisso?
E se eu precisar de uma solução de baixo consumo?
Comparativo
Conclusão
É raro, mas acontece muito!
Aqui é uma "brincadeira", um exagero, mas de fato já presenciei coisas similares e mais de uma vez.
Vou usar o famoso hello world
para não entrar em detalhes dos casos, mas os casos reais eram quase "tão simples quanto".
Inclusive já teve casos em que de fato nem um banco de dados usavam, como no exemplo hipotético que vou mostrar.
Aqui não é uma crítica ao spring, inclusive é o framework que mais uso, é só um exemplo didático.
O dev vai lá no Spring Initializr e sai escolhendo as dependências:
Ok, você baixou um demo.zip de cerca de 70kb.
A saga começa
Se você quiser pular essa parte, pode ir direto aqui
Você vai no root do projeto e executa:
mvn install
Dá uns erros do test do Spring, você dá um Google
e sem entender direito acaba adicionando a dependência do H2 e configurando o application.properties
assim:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
Você roda o mvn novamente e... Sucesso!
Então você abre sua IDE preferida e implementa seu endpoint.
(aqui um Hello world
simples, porque não é esse o foco!)
package com.example.demo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class HelloController {
@GetMapping("/my-api")
ResponseEntity<String> myApi() {
return ResponseEntity.ok("hello world!");
}
}
Então você tenta executar seu projeto, dá um erro do docker compose. Você descobre que não vai precisar dele agora, remove a dependência do pom.xml
e Sucesso!
Agora muito orgulho você vai acessar sua API
(ex: http://localhost:8080/my-api)
Então uma tela estranha de login aparece, você dá um Google e acaba adicionando esse bean.
(o qual me lembra muito os CORS que aceitam tudo 😂)
...
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests((authorize) -> authorize.anyRequest()
.permitAll())
.build();
}
...
Você acessa novamente a sua API e Sucesso??
(Sério mesmo que se nesse ponto você ainda não começou a questionar de que algo de errado não está certo 🤔)
Para quem não sabe onde quer ir, qualquer lugar serve.
É compreensivo que dependendo do seu nível de conhecimento sobre algo, você não conheça tão "profundamente".
Mas é importante ter uma ideia básica do que está sendo feito.
Propositalmente nesse exemplo foi adicionado várias dependências desnecessárias, mas muito comuns em tutoriais.
Quando for fazer seu projeto é importante ter alguma noção do que cada uma delas faz ou que recursos ela trará para o projeto.
Se é algo que você não precisará não faz sentido incluir ela.
Outro ponto muito importante, que vai além do código, é entender que problema seu projeto vai resolver. Com isso em mente você pode balancear entre "facilidade de desenvolvimento" e "operação".
É preciso compreender além do código quando você decide criar um novo projeto. Mesmo que seja "apenas" um módulo dentro de outro projeto já existente.
Ah, mais o projeto ficou só com 70kb, nem vale a pena pensar nisso!
Vamos dissecar o assunto?
Dissecando a primeira solução
Se o "demo.zip" ficou só com 70kb é perda de tempo olhar para isso, certo?
Se isso parece correto para você, muito provavelmente você não está olhando para além do código.
Para facilitar o entendimento eu criei um maven repo
vazio antes de começar esse projeto de exemplo. Tendo só esse projeto o repositório ficou com cerca de 157mb.
Além disso, como nesse caso é uma aplicação spring-boot, você terá como "executável" um arquivo jar de 61mb.
Mais ainda estamos falando de 218mb, disco hoje é barato!
Muita hora nessa calma!
Entendendo outros aspectos além do código
Boas decisões dependem de boas informações, seja o CEO da sua empresa ou você nesse projeto.
Se faltam informações, busque-as primeiro e evite a famosa vontade de sair fazendo, pois depois que já tiver feito algo, provavelmente seu "apego" à ele irá te impedir de fazer a "coisa certa."
Esse projeto terá muita alteração de código?
Muita manutenção?
Se a resposta for sim, o lado da balança referente à "facilidade de desenvolvimento" precisa ser considerada.
Onde seu projeto vai ser executado?
- Servidor próprio?
- Cloud?
- Será serverless?
- Fica em um servidor só ou é distribuído em vários?
- Algum hardware embarcado?
Embora a primeira solução possa rodar em qualquer uma dessas situações, cada cenário tem pontos de relevância diferentes.
Vamos executar o projeto e buscar mais alguns dados.
O startup time ficou em cerca de 6.5 segundos e alocou 318mb de memória ram.
Se isso for rodar em um servidor próprio, com recursos ociosos e não é um "serverless" então pode ser mesmo que não vale a pena pensar em algo mais "adequado" do ponto de vista de consumo de recursos.
Se for "serverless" o startup time se torna relevante e virará um ponto de atenção. O consumo de memória também pode ser um fator importante.
Se for "Cloud", seja serverless ou não, esses pontos devem impactar diretamente na fatura no final do mês.
Se forem vários servidores próprios distribuídos em sua rede, o deploy disso pode ser outro ponto de atenção, principalmente se tiverem locais com alguma limitação de rede.
Se for um hardware embarcado, muito provavelmente otimizar os recursos será importante.
Com as respostas em mãos agora você pode pensar em uma solução, que pode envolver outro framework mais "adequado" ou até mesmo uma solução sem uso de um framework.
Vamos apimentar isso?
Imagine que agora você precisa criar uma tela "web" para o usuário informar algo que a API precise como input.
Na ideia de simplificar esse caso de exemplo, vamos dizer que agora você precisa pegar o nome da pessoa para que a API retorne Hello <nome da pessoa>!
ao invés de Hello world!
Então rapidamente você vai no https://nextjs.org/ copia o comando e sai montando seu frontend.
npx create-next-app@latest
Responde algumas perguntas, depois cria sua "tela" pedindo o nome da pessoa.
Para não ficar repetitivo não vamos dissecar esse ponto também, mas apenas para dar uma ideia, o diretório desse projeto antes mesmo de criar o código da tela já vai ter algo em torno de 427mb.
A ideia aqui se repete com o exemplo do spring-boot, basicamente esse tamanho é devido a várias dependências que vão estar na famosa node_modules
.
O tamanho do no diretório .next
após rodar npm run build
fica em cerca de 28mb. Se "descontarmos" o diretório "cache" ainda ficam cerca de 2mb.
E se eu precisar de uma solução de baixo consumo?
Vamos considerar que por N motivos você precise de rápido startup time, baixo consumo de memória e baixo consumo de disco tanto no deploy quanto onde a solução estará sendo executada.
Aqui novamente não vamos aprofundar no código, não é o foco, mas sim na abordagem da solução.
Surgem algumas dúvidas:
- Preciso de todas essas dependências que vejo nos tutoriais?
- Preciso de um framework moderno e cheio de recursos avançados para esse caso de frontend?
- Preciso de um framework de backend?
Afinal o que eu preciso?
Um endpoint como esse do exemplo precisa estar rodando em algum "computador" que responda à um IP e porta.
Um frontend como esse, que será acessado via uma URL, precisa ser algo que qualquer navegador consiga interpretar.
E se fizermos esse frontend, que é simples, no bom e velho html + css + javascript
?
Os frameworks de frontend como o NextJs, no final geram arquivos estáticos, logo seria possível chegar no "mesmo resultado" usando a proposta acima. Isso sem entrar no mérito de um backend for frontend (que vai ter SSR, SSG, etc)
Como o frontend não será hospedado com um nodejs
, esses recursos de qualquer forma já não seriam aproveitados.
Esse é um exemplo simples, sem focar no design ou na funcionalidade, pois não é foco.
Arquivo: hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Demo</title>
<style>
.content {
display: grid;
grid-template-columns: auto auto;
gap: 10px;
padding: 10px;
background: aliceblue;
}
.end-row {
text-align: end;
}
.start-2-row {
grid-column-start: 2;
}
.title-row {
grid-column-start: 1;
grid-column-end: 3;
text-align: center;
font-weight: 900;
font-size: 2rem;
}
input[type="text"] {
height: 21px;
}
button {
height: 24px;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", (event) => {
document.querySelector("#send").addEventListener('click', (event) => {
const name = document.querySelector("#name").value;
fetch(`/api/my-api?name=${name}`).then((resp) => resp.text().then((data) => alert(data)));
});
});
</script>
</head>
<body>
<div class="content">
<div class="title-row">Hello Demo</div>
<div class="end-row">Qual seu nome?</div>
<div><input type="text" id="name" placeholder="Informe seu nome"></div>
<div class="start-2-row">
<button id="send">Enviar</button>
</div>
</div>
</body>
</html>
E se eu usar algo "nativo" da tecnologia para ser meu "servidor web"?
Existem outras opções menos "baixo nível", mas para esse exemplo iremos implementar algo sem nenhum framework ou biblioteca terceira. Embora essa seja uma implementação 100% funcional ela é apenas para fins de exemplo.
Arquivo DemoSimplificado.java
(web server e backend da aplicação)
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
public class DemoSimplificado {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
var port = Integer.parseInt(System.getProperty("port", "8080"));
var rootDirectory = Path.of("./static").toAbsolutePath();
var server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/api", new ApiHandler());
server.createContext("/app", new AppHandler(rootDirectory));
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
System.out.printf("Serving on port %d (Started in %dms)%n", port, (System.currentTimeMillis() - start));
}
static abstract class ServerHandler implements HttpHandler {
protected abstract void processRequest(HttpExchange exchange, Map<String, String> queryParams) throws IOException;
static void sendError(HttpExchange exchange, int code, String message) throws IOException {
exchange.sendResponseHeaders(code, message.length());
try(var output = exchange.getResponseBody()) {
output.write(message.getBytes());
}
}
static Map<String, String> getQueryParams(final HttpExchange exchange) {
Map<String, String> queryParams = new HashMap<>();
var urlParts = exchange.getRequestURI().toString().split("\\?");
var queryParamsList = urlParts.length > 1 ? urlParts[1].split("&") : null;
if (queryParamsList != null) {
for (var item : queryParamsList) {
var keyValue = item.split("=");
var value = keyValue.length == 1 ? null : keyValue[1];
queryParams.put(keyValue[0], value);
}
}
return queryParams;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if(!"GET".equals(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
if (exchange.getRequestURI() == null) {
sendError(exchange, 400, "Bad request");
return;
}
var queryParams = getQueryParams(exchange);
System.out.printf("%nURL:%s%nQuery Params: %s%n", exchange.getRequestURI(), queryParams);
processRequest(exchange, queryParams);
}
}
static class ApiHandler extends ServerHandler {
@Override
public void processRequest(HttpExchange exchange, Map<String, String> queryParams) throws IOException {
var name = queryParams.get("name") != null ? queryParams.get("name") : "world";
var response = "Hello %s!".formatted(name);
exchange.sendResponseHeaders(200, response.length());
try (OutputStream output = exchange.getResponseBody()) {
output.write(response.getBytes());
}
}
}
static class AppHandler extends ServerHandler {
private final Path rootDirectory;
public AppHandler(final Path rootDirectory) {
this.rootDirectory = rootDirectory;
}
@Override
public void processRequest(HttpExchange exchange, Map<String, String> queryParams) throws IOException {
var filePath = Paths.get(rootDirectory.toString(), exchange.getRequestURI().getPath().replaceFirst("/app", ""));
var file = filePath.toFile();
if (!file.exists()) {
sendError(exchange, 404, "Not found");
}
exchange.sendResponseHeaders(200, file.length());
try (OutputStream output = exchange.getResponseBody()) {
Files.copy(file.toPath(), output);
}
}
}
}
Vamos aos números da solução
Não utiliza bibliotecas, nem frameworks, nem mesmo maven, então sobra só os fontes e o executável jar.
Fontes (36Kb):
(Já incluindo frontend, manifesto, projeto do IntelliJ IDEA, etc)
Executável Jar (8Kb):
(Já incluindo frontend)
Execução consumindo cerca de 46mb de memória ram, já incluindo o frontend.
Tanto a primeira solução quanto essa, poderiam ter o "Xmx" configurado para limitar o uso de memória, mas não iremos aprofundar nisso agora.
Startup time abaixo de 30ms.
Comparativo
A solução 2 é bem simplista, por isso vamos comparar ela com a primeira solução e também "multiplicando por 10 vezes".
1a Solução (frameworks) | 2a Solução | 2a Solução (10x) | |
---|---|---|---|
Startup time | 6.527 ms | 27ms | 270ms |
Consumo de memória | 318mb | 46mb | 460mb |
Tamanho Projeto e Dependências | 584 mb (157mb + 427mb ) | 36kb | 360kb |
Tamanho Build/executável | 63mb (61mb + 2mb) | 8kb | 80kb |
Conclusão
É claro que os frameworks são ótimos aliados e bem vindos.
A "provocação" desse post é sobre ir além do código e entender a solução que você está entregando, que ela eventualmente sairá do automático "usando o framework do coração" e que a solução pode até mesmo nem utilizar um framework.
O que você acha sobre isso?
Top comments (0)