Como Usar Arquitetura de Frontend: Flux, MVC, MVVM e Event-Driven Design em Produção Já leu

MVC: O Fundamento Model-View-Controller é o padrão mais tradicional em arquitetura de frontend, separando responsabilidades em três camadas. O Model gerencia dados, a View exibe informações e o Controller orquestra a comunicação entre elas. O fluxo é bidirecional: usuário interage com a View, Controller captura a ação, atualiza o Model, e o Model notifica a View para re-renderizar. Esse ciclo, apesar de simples, pode gerar acoplamento se não for bem implementado. Vamos a um exemplo prático em JavaScript puro: ${user.name} O MVC funciona bem em projetos pequenos e médios, mas começa a mostrar limitações em aplicações complexas onde múltiplas views precisam sincronizar com o mesmo modelo. MVVM: Binding Automático Model-View-ViewModel introduz uma camada intermediária que elimina a comunicação direta entre View e Model. O ViewModel expõe dados reativos que a View observa automaticamente, criando um two-way binding. Diferentemente do MVC, você não precisa notificar manualmente a View. Frameworks como Vue.js implementam MVVM nativamente. O ViewModel contém toda a lógica de

MVC: O Fundamento

Model-View-Controller é o padrão mais tradicional em arquitetura de frontend, separando responsabilidades em três camadas. O Model gerencia dados, a View exibe informações e o Controller orquestra a comunicação entre elas.

O fluxo é bidirecional: usuário interage com a View, Controller captura a ação, atualiza o Model, e o Model notifica a View para re-renderizar. Esse ciclo, apesar de simples, pode gerar acoplamento se não for bem implementado. Vamos a um exemplo prático em JavaScript puro:

// Model
class UserModel {
  constructor() {
    this.users = [];
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  addUser(name) {
    this.users.push({ id: Date.now(), name });
    this.notifyObservers();
  }

  notifyObservers() {
    this.observers.forEach(obs => obs.update(this.users));
  }
}

// View
class UserView {
  constructor(elementId) {
    this.element = document.getElementById(elementId);
  }

  render(users) {
    this.element.innerHTML = users
      .map(user => `<div>${user.name}</div>`)
      .join('');
  }

  update(users) {
    this.render(users);
  }
}

// Controller
class UserController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    this.model.addObserver(this.view);
  }

  handleAddUser(name) {
    this.model.addUser(name);
  }
}

// Uso
const model = new UserModel();
const view = new UserView('app');
const controller = new UserController(model, view);
controller.handleAddUser('João');

O MVC funciona bem em projetos pequenos e médios, mas começa a mostrar limitações em aplicações complexas onde múltiplas views precisam sincronizar com o mesmo modelo.

MVVM: Binding Automático

Model-View-ViewModel introduz uma camada intermediária que elimina a comunicação direta entre View e Model. O ViewModel expõe dados reativos que a View observa automaticamente, criando um two-way binding.

Diferentemente do MVC, você não precisa notificar manualmente a View. Frameworks como Vue.js implementam MVVM nativamente. O ViewModel contém toda a lógica de apresentação, deixando a View completamente declarativa. Aqui está um exemplo com Vue 3:

// ViewModel + View combinados
<template>
  <div>
    <input v-model="userInput" placeholder="Nome" />
    <button @click="addUser">Adicionar</button>
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// Model
const users = ref([]);
const userInput = ref('');

// ViewModel (aqui mesmo)
const addUser = () => {
  if (userInput.value.trim()) {
    users.value.push({ 
      id: Date.now(), 
      name: userInput.value 
    });
    userInput.value = '';
  }
};
</script>

O MVVM reduz boilerplate e torna o código mais intuitivo para quem trabalha com interfaces reativas. Porém, em aplicações com estado global complexo, ele pode criar ViewModels inchados e difíceis de testar.

Flux: Unidirecionalidade Garantida

Flux é uma arquitetura, não um padrão, que resolve o problema de estado compartilhado através de um fluxo de dados unidirecional: Actions → Dispatcher → Stores → Views. Quando uma View precisa mudar estado, dispara uma Action, nunca modifica diretamente o Store.

Essa rigidez torna o código previsível e facilita debug. Redux popularizou essa ideia, adicionando imutabilidade e uma store centralizada. Veja um exemplo funcional:

// Action Creator
const ADD_USER = 'ADD_USER';

function addUserAction(name) {
  return { type: ADD_USER, payload: name };
}

// Reducer (implementa a lógica)
function usersReducer(state = [], action) {
  switch(action.type) {
    case ADD_USER:
      return [...state, { 
        id: Date.now(), 
        name: action.payload 
      }];
    default:
      return state;
  }
}

// Store (Redux simplificado)
class SimpleStore {
  constructor(reducer) {
    this.reducer = reducer;
    this.state = reducer(undefined, {});
    this.listeners = [];
  }

  dispatch(action) {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach(listener => listener(this.state));
  }

  subscribe(listener) {
    this.listeners.push(listener);
  }

  getState() {
    return this.state;
  }
}

// Uso
const store = new SimpleStore(usersReducer);

store.subscribe(state => {
  console.log('Estado atualizado:', state);
});

store.dispatch(addUserAction('Maria'));

Flux é ideal para aplicações médias e grandes onde múltiplos componentes compartilham estado. A desvantagem é verbosidade inicial e curva de aprendizado.

Event-Driven Design: Desacoplamento Total

Em arquiteturas event-driven, componentes comunicam-se através de eventos, sem conhecimento direto uns dos outros. Um Publisher emite eventos, Subscribers reagem. Isso permite escalabilidade e flexibilidade máximas.

Diferencia-se de Flux porque não há ordem fixa: eventos podem ser capturados por múltiplos listeners, e novos listeners podem ser adicionados sem modificar o sistema. É especialmente poderoso em aplicações complexas e microsserviços frontend. Exemplo prático:

// Event Emitter pattern
class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  emit(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(data));
    }
  }
}

// Componentes desacoplados
const bus = new EventBus();

// Componente que cria usuário
class UserCreator {
  constructor(bus) {
    this.bus = bus;
  }

  create(name) {
    this.bus.emit('user:created', { name, id: Date.now() });
  }
}

// Componente que reage
class UserLogger {
  constructor(bus) {
    bus.on('user:created', (user) => {
      console.log('Novo usuário:', user.name);
    });
  }
}

// Uso
const creator = new UserCreator(bus);
const logger = new UserLogger(bus);

creator.create('Pedro'); // Log: "Novo usuário: Pedro"

Event-driven é menos estruturado que Flux, exigindo disciplina para não virar "callback hell". Ideal para aplicações modulares onde componentes aparecem e desaparecem dinamicamente.

Conclusão

Cada arquitetura resolve problemas específicos: MVC é direto e tradicional para projetos simples; MVVM oferece reatividade elegante em frameworks modernos; Flux garante previsibilidade em aplicações complexas com estado global; Event-Driven maximiza desacoplamento e flexibilidade. Na prática, você combina essas abordagens: um app pode usar Flux para estado global, MVVM em componentes e event-driven para cross-cutting concerns. O segredo é escolher a ferramenta certa para cada camada.

Referências


Artigos relacionados