E se ao invés de usar classes, instâncias, camadas e MVC nós voltarmos aos fundamentos do JavaScript e construir aplicações inteiras somente com funções?
Slides da palestra sobre a arquitetura da Netflix, demonstrando como nós usamos Componentes Funcionais e React para construir nossas aplicações.
Apresentada no TDC Porto Alegre 2016.
Vídeo e mais informações em http://bruno.tavares.me
31. Funções Puras
function formatTitle(title) {
return `<h1>${title}</h1>`;
}
Entrada
Saída
Para os mesmos parâmetros,
sempre o mesmo resultado
// ...
formatTitle('Narcos'); // <h1>Narcos</h1>
O objetivo dessa apresentação é compartilhar como nós usamos programação funcional e componentes React para construir uma arquitetura escalável.
Mas antes…
3 curiosidades sobre a Netflix que você não sabia!
#1
Nós amamos JavaScript!
Netflix roda em milhares de dispositivos…
…JavaScript também! Nós temos essa linguagem tão versátil que nos permite pular de plataforma para plataforma com o mínimo de fricção.
#2
Nós temos uma super secreta plataforma para TV.
Ela se chama Gibbon, e é uma camada nativa controlada por código JavaScript.
Nossa aplicação antiga era toda baseada em HTML, CSS e JS. Ela rodava até em navegadores Webkit, mas nós empacotávamos tudo para as televisões.
Porém nós temos uns dispositivos um tanto quanto desafiadores. Esses streaming sticks, por exemplo, custam apenas $30, tem um processador single core de 600mhz, JavaScript Core sem just-in-time compilation, e é 465x mais lento que o V8 no meu laptop.
Então nós paramos de usar webkit, e criamos uma plataforma própria que se assemelha muito, porém toda arquitetada para performance e que ainda suportasse uma UI em JavaScript.
O código parece muito similar, porém o código HTML é totalmente declarativo, enquanto no Gibbon nós usamos chamadas imperativas no JavaScript.
#3
Nós lançamos tudo com testes A/B.
Nós testamos experiências novas em uma parcela dos nossos usuários.
Caso a nova experiência demonstre métricas positivas…
…nós lançamos em produção para todos usuários.
E nos fazemos isso tanto para mudanças grandes, como também para pequenas optimizações. Então imagine a quantidade de alterações que cada componente pode receber, e quantas permutações a nossa interface precisa suportar.
As decisões que nossa equipe toma com relação à arquitetura estão diretamente relacionadas à velocidade e volume de testes A/B, que por sua vez impactam a nossa abilidade de inovar. Isso reforça o valor de ter uma boa arquitetura desde o começo, porque essa decisão afeta em muito o nosso produto.
Entrando mais na parte técnica, a nossa arquitetura anterior funcionava com o bom e velho MVC. O controller recebe eventos da view, altera o model, que por sua vez dispara um evento, a view escuta e se atualiza, e tudo funciona muito bem.
Porém 6 anos e centenas de testes A/B depois, a nossa arquitetura se parece mais com isso.
A medida que a aplicação foi crescendo, muitos canais secundários foram criando-se e ficou muito difícil compreender o fluxo do código, das dependências, e mais importante, das mutações do estado da aplicação.
Nesse ponto começamos a nos perguntar se estávamos entendedo JavaScript errado.
Viemos do mundo dos servidores aonde a requisição começa, executa e termina. Uma nova requisição começa tudo do zero, e a probabilidade de entrar em um mal estado é muito menor.
Porém no front-end o nosso usuário pode sentar no sofá na sexta e só parar a maratona de Narcos no domingo! Quem consegue prever o estado da aplicação 3 dias depois de inicializada?
Então nós chegamos a conclusão que estado mutável e compartilhado é a raiz de todos os males. Com tantos canais secundários na nossa arquitetura MVC, era impossível acompanhar o fluxo de alterações.
Partes da nossa aplicação tinha o ”expert” dedicado, que era a pessoa que entendia daquela área e estava melhor qualificada para realizar alterações, já que ela compreendia melhor os diferentes estados. Isso era altamente indesejável e engessava nossa abilidade de inovar.
Então nós começamos experimentos, saindo de uma arquitetura onde tudo eram objetos e camadas, e começamos a construir nosso código como um fluxo de componentes funcionais.
Nós começamos a utilizar mais conceitos de JavaScript Funcional ao invés de Orientação à Objetos.
Em sua definição, o paradigma de programação funcional dita que o código seja expresso de forma declarativa, em pequenas funções, evitando alteração de estado e dados mutáveis.
Só nessa pequena definição nós encontramos uma série de palavras-chave altamente desejáveis para nossa nova arquitetura.
Então a partir dos nossos experimentos nós definimos 4 princípios para a nossa arquitetura.
#1 Componentes Puros
Funções puras são um dos alicerces da programação funcional. Elas são excelentes por serem previsíveis, idependende de qualquer estado exterior - e portanto imune à muitos erros introduzidos por estados compartilhados – e fáceis de refatorar, reorganizar, mover, e compôr.
Se nós conseguissemos aplicar o mesmo para componentes, e criar componentes puros, nós poderíamos extrair as mesmas qualidades da função pura.
Para isso nós usamos React. Ele é um framework que nos permite aplicar conceitos de programação funcional e construir interfaces inteiras somente com funções.Esse é um pequeno exemplo de um componente puro. Ele não tem estado nenhum, somente renderiza os valores passados por `props`. Para consumir, nós podemos invocar ele como qualquer outra função.
Ele é tão mesmo uma função, que o React nos permite reescrevê-lo dessa forma. Se você analizar, é o mesmo conceito da função pura, aonde nós temos uma entrada, uma saída, e nenhum estado.Dessa forma nós conseguimos ter um componente puro, e usá-lo como alicerce da nossa arquitetura.
Componentes puros garantem previsibilidade, já que para as mesmas entradas, sempre terão a mesma saída. Eles são independentes de estado, fáceis de refatorar e podem ser compostos em componentes cada vez mais complexos.
#2 API Declarativa
API Declarativas nos proporcionam código mais conciso, legível e de fácil compreensão.
Ao invés de dizer exatamente como fazer, nós apenas expressamos o que queremos fazer.
Definir hierarquias também é muito melhor na forma declarativa, do que um código imperativo que levaria dezenas de linhas de código.
No final das contas, ela é uma forma mais legível, de fácil compreensão e com menos margem para bugs.
#3 Composição
Nosso objetivo não é só ficar em componentes pequenos, mas compôr eles de modo a criar a aplicação inteira de forma pura e funcional, onde dados entram no topo e a aplicação completa sai como retorno.
Nós podemos também extender um componentes com mais funcionalidades usando composição ao invés de criar classes e extendê-las (herança).
Composição nos proporciona escalabilidade. Também nos permite criar interfaces complexas de forma controlada. Alterações em um componente base não afetam àqueles que o extendem, já que eles usam composição e não herança.
#4 Fluxo Unidirecional
React nos permite voltar à maneira de trabalhar que usávamos nos velhos tempos de aplicações PHP.
Quando os dados mudam…
Nós jogamos tudo fora e renderizamos de novo.
Bacana… e como eu executo efeitos colaterais?
Nós usamos ações e redutores. Quando algum efeito colateral (uma troca de rota, uma requisição, um evento de teclado, etc…) acontece, uma ação é disparada para representar aquele efeito.O redutor então verifica se para aquela ação é preciso alterar o estado. Se for preciso, ele vai alterar o estado – que é imutável – e receberá o novo estado. O novo estado é então passado para os componentes puros que criar a nova interface.
E se você pudesse criar toda sua aplicação, só com componentes puros?
Essa é a galeria do Netflix. Nós temos o componente Gallery. Cada linha é um componente Category. Cada filme é um componente Boxshot.
A nossa estrutura de dados parece-se com isso.
Nosso primeiro componente puro Boxshot recebe as props e retorna a nova view.
Nosso componente categoria faz o mesmo. Ele também mapeia todos os filmes para componentes Boxshot.
E a galeria faz o mesmo, mapeando categorias para o componente Category.
No fim nós temos nossa interface inteira, somente com componentes puros!
E com isso nós finalizamos a base da arquitetura da Netflix. Ela é previsível, declarativa, extensível, unidirecional e imutável.
Ela nos permite fazer essa expressão uma realidade, aonde o nosso aplicativo é um produto dos dados, passado à uma função que retorna toda a view.