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.