Sempre vale apena usar Frameworks?
Tags: programming, productivity, discuss, frameworks
Published: 2024-02-16 22:00:19

É 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!](#é-raro-mas-acontece-muito)
[Para quem não sabe onde quer ir, qualquer lugar serve](#para-quem-não-sabe-onde-quer-ir-qualquer-lugar-serve)
[Dissecando a primeira solução](#dissecando-a-primeira-solução)
[Entendendo outros aspectos além do código](#entendendo-outros-aspectos-além-do-código)
[Vamos colocar mais pimenta nisso?](#vamos-colocar-mais-pimenta-nisso)
[E se eu precisar de uma solução de baixo consumo?](#e-se-eu-precisar-de-uma-solução-de-baixo-consumo)
[Comparativo](#comparativo)
[Conclusão](#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](https://start.spring.io/) e sai escolhendo as dependências:

![Dependência no Spring Initializr](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7ye8lakrmzvqxc3oqhn.png)

Ok, você baixou um demo.zip de cerca de 70kb.

### A saga começa
_Se você quiser pular essa parte, pode ir_ [direto aqui](#para-quem-não-sabe-onde-quer-ir-qualquer-lugar-serve)

Você vai no root do projeto e executa:
```bash
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!)
```java
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 😂)
```java
...
@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.
![Tamanho do mvn_repo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vm5qepuzcy8fnlnfgr0m.png)

Além disso, como nesse caso é uma aplicação spring-boot, você terá como "executável" um arquivo jar de 61mb.
![Tamanho do executável jar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r3izzbrdolk9rp5ui9b9.png)

_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?**
1. Servidor próprio?
2. Cloud?
3. Será serverless?
4. Fica em um servidor só ou é distribuído em vários?
5. 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.

![Iniciando demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/heahu0qht730gp8nftk3.png)

![Startup time](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vaqx78ttfgcgzq1kf84q.png)

![Memória alocada](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5nofcib1dysxano07f2k.png)

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_.
```bash
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`.

![Tamanho projeto inicial NextJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/66urigg3s5tdl1excof2.png)

![Tamanho .next](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gch3pg0hf4q5ax5d47az.png)

![Tamanho .next/cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/veux2lt6kcaf5steayw6.png)

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
```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)
```java
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)

![Solução 2 fontes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3up5s3kog5fsg4azfes.png)


Executável Jar (8Kb):
(Já incluindo _frontend_)

![Solução 2 executável](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xsoiahv68hnao1nmqfvb.png)

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.

![Solução 2 execução](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fc2n9c5rbjiqgggythp3.png)

![Solução 2 memória](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/83nrjosyj4kbcpjyly3t.png)

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?