Architectural Patterns in JavaScript
Architectural patterns provide high-level structure for applications. This article covers MVC, MVVM, MVP, Flux, and Clean Architecture patterns.
Introduction
Architectural patterns provide:
- Application structure
- Separation of concerns
- Scalability
- Maintainability
- Testability
Understanding these patterns helps you:
- Design scalable systems
- Organize code effectively
- Manage complexity
- Improve collaboration
- Build sustainable applications
Model-View-Controller (MVC)
Basic MVC
// โ
Good: Basic MVC pattern
class Model {
constructor() {
this.data = {};
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
setData(key, value) {
this.data[key] = value;
this.notify();
}
getData(key) {
return this.data[key];
}
}
class View {
constructor(model) {
this.model = model;
this.model.subscribe(this);
}
update() {
this.render();
}
render() {
console.log('Rendering view with data:', this.model.data);
}
}
class Controller {
constructor(model, view) {
this.model = model;
this.view = view;
}
handleUserInput(key, value) {
this.model.setData(key, value);
}
}
// Usage
const model = new Model();
const view = new View(model);
const controller = new Controller(model, view);
controller.handleUserInput('name', 'John');
// Rendering view with data: { name: 'John' }
MVC with Multiple Views
// โ
Good: MVC with multiple views
class UserModel {
constructor() {
this.users = [];
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
addUser(user) {
this.users.push(user);
this.notify();
}
getUsers() {
return this.users;
}
}
class ListView {
constructor(model) {
this.model = model;
this.model.subscribe(this);
}
update() {
console.log('List View:', this.model.getUsers());
}
}
class TableView {
constructor(model) {
this.model = model;
this.model.subscribe(this);
}
update() {
console.log('Table View:', this.model.getUsers());
}
}
class UserController {
constructor(model) {
this.model = model;
}
addUser(user) {
this.model.addUser(user);
}
}
// Usage
const model = new UserModel();
const listView = new ListView(model);
const tableView = new TableView(model);
const controller = new UserController(model);
controller.addUser({ name: 'John' });
// List View: [{ name: 'John' }]
// Table View: [{ name: 'John' }]
Model-View-ViewModel (MVVM)
// โ
Good: MVVM pattern
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserViewModel {
constructor(model) {
this.model = model;
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
get displayName() {
return `${this.model.name} (${this.model.email})`;
}
set displayName(value) {
const [name, email] = value.split(' (');
this.model.name = name;
this.model.email = email.replace(')', '');
this.notify();
}
get name() {
return this.model.name;
}
set name(value) {
this.model.name = value;
this.notify();
}
get email() {
return this.model.email;
}
set email(value) {
this.model.email = value;
this.notify();
}
}
class UserView {
constructor(viewModel) {
this.viewModel = viewModel;
this.viewModel.subscribe(this);
}
update() {
this.render();
}
render() {
console.log(`Name: ${this.viewModel.name}`);
console.log(`Email: ${this.viewModel.email}`);
console.log(`Display: ${this.viewModel.displayName}`);
}
}
// Usage
const user = new User('John', '[email protected]');
const viewModel = new UserViewModel(user);
const view = new UserView(viewModel);
viewModel.name = 'Jane';
// Name: Jane
// Email: [email protected]
// Display: Jane ([email protected])
Model-View-Presenter (MVP)
// โ
Good: MVP pattern
class UserModel {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
}
class UserPresenter {
constructor(model, view) {
this.model = model;
this.view = view;
}
onViewLoaded() {
const name = this.model.getName();
const email = this.model.getEmail();
this.view.displayUser(name, email);
}
onNameChanged(name) {
this.model.name = name;
this.view.displayUser(this.model.getName(), this.model.getEmail());
}
}
class UserView {
constructor(presenter) {
this.presenter = presenter;
}
displayUser(name, email) {
console.log(`User: ${name} (${email})`);
}
onNameInputChanged(name) {
this.presenter.onNameChanged(name);
}
}
// Usage
const model = new UserModel('John', '[email protected]');
const view = new UserView(null);
const presenter = new UserPresenter(model, view);
view.presenter = presenter;
presenter.onViewLoaded();
// User: John ([email protected])
view.onNameInputChanged('Jane');
// User: Jane ([email protected])
Flux Architecture
// โ
Good: Flux architecture
class Store {
constructor() {
this.state = {};
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
getState() {
return this.state;
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.notify();
}
}
class Dispatcher {
constructor(store) {
this.store = store;
}
dispatch(action) {
switch (action.type) {
case 'ADD_USER':
this.store.setState({
users: [...(this.store.getState().users || []), action.payload]
});
break;
case 'REMOVE_USER':
this.store.setState({
users: this.store.getState().users.filter(u => u.id !== action.payload)
});
break;
}
}
}
class View {
constructor(store, dispatcher) {
this.store = store;
this.dispatcher = dispatcher;
this.store.subscribe(this);
}
update() {
this.render();
}
render() {
console.log('Users:', this.store.getState().users);
}
addUser(user) {
this.dispatcher.dispatch({
type: 'ADD_USER',
payload: user
});
}
}
// Usage
const store = new Store();
const dispatcher = new Dispatcher(store);
const view = new View(store, dispatcher);
view.addUser({ id: 1, name: 'John' });
// Users: [{ id: 1, name: 'John' }]
view.addUser({ id: 2, name: 'Jane' });
// Users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
Clean Architecture
Layered Architecture
// โ
Good: Clean architecture with layers
// Domain Layer (Business Logic)
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
isValid() {
return this.name && this.email.includes('@');
}
}
// Application Layer (Use Cases)
class CreateUserUseCase {
constructor(repository) {
this.repository = repository;
}
execute(name, email) {
const user = new User(null, name, email);
if (!user.isValid()) {
throw new Error('Invalid user data');
}
return this.repository.save(user);
}
}
// Infrastructure Layer (Data Access)
class UserRepository {
constructor() {
this.users = [];
}
save(user) {
user.id = this.users.length + 1;
this.users.push(user);
return user;
}
findById(id) {
return this.users.find(u => u.id === id);
}
}
// Presentation Layer (Controllers)
class UserController {
constructor(createUserUseCase) {
this.createUserUseCase = createUserUseCase;
}
createUser(name, email) {
try {
const user = this.createUserUseCase.execute(name, email);
return { success: true, user };
} catch (error) {
return { success: false, error: error.message };
}
}
}
// Usage
const repository = new UserRepository();
const useCase = new CreateUserUseCase(repository);
const controller = new UserController(useCase);
const result = controller.createUser('John', '[email protected]');
console.log(result);
// { success: true, user: User { id: 1, name: 'John', email: '[email protected]' } }
Dependency Inversion
// โ
Good: Clean architecture with dependency inversion
class UserService {
constructor(repository, emailService, logger) {
this.repository = repository;
this.emailService = emailService;
this.logger = logger;
}
createUser(userData) {
this.logger.log(`Creating user: ${userData.name}`);
const user = this.repository.save(userData);
this.emailService.sendWelcomeEmail(user.email);
this.logger.log(`User created: ${user.id}`);
return user;
}
}
// Interfaces (contracts)
class IRepository {
save(data) { throw new Error('Not implemented'); }
}
class IEmailService {
sendWelcomeEmail(email) { throw new Error('Not implemented'); }
}
class ILogger {
log(message) { throw new Error('Not implemented'); }
}
// Implementations
class UserRepository extends IRepository {
save(data) {
console.log('Saving to database');
return { id: 1, ...data };
}
}
class EmailService extends IEmailService {
sendWelcomeEmail(email) {
console.log(`Sending welcome email to ${email}`);
}
}
class Logger extends ILogger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
// Usage
const repository = new UserRepository();
const emailService = new EmailService();
const logger = new Logger();
const service = new UserService(repository, emailService, logger);
service.createUser({ name: 'John', email: '[email protected]' });
Practical Examples
Todo Application
// โ
Good: Todo app with MVC
class TodoModel {
constructor() {
this.todos = [];
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify() {
this.observers.forEach(observer => observer.update());
}
addTodo(text) {
this.todos.push({ id: Date.now(), text, completed: false });
this.notify();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.notify();
}
}
getTodos() {
return this.todos;
}
}
class TodoView {
constructor(model) {
this.model = model;
this.model.subscribe(this);
}
update() {
this.render();
}
render() {
console.log('Todos:');
this.model.getTodos().forEach(todo => {
const status = todo.completed ? 'โ' : 'โ';
console.log(` ${status} ${todo.text}`);
});
}
}
class TodoController {
constructor(model) {
this.model = model;
}
addTodo(text) {
this.model.addTodo(text);
}
toggleTodo(id) {
this.model.toggleTodo(id);
}
}
// Usage
const model = new TodoModel();
const view = new TodoView(model);
const controller = new TodoController(model);
controller.addTodo('Learn JavaScript');
controller.addTodo('Build an app');
controller.toggleTodo(model.todos[0].id);
Best Practices
-
Separate concerns:
// โ Good class Model { } class View { } class Controller { } // โ Bad class Everything { } -
Use dependency injection:
// โ Good class Service { constructor(repository) { this.repository = repository; } } // โ Bad class Service { constructor() { this.repository = new Repository(); } } -
Keep layers independent:
// โ Good // Domain doesn't depend on Infrastructure // โ Bad // Domain imports from Infrastructure
Common Mistakes
-
Mixing concerns:
// โ Bad class User { save() { } render() { } validate() { } } // โ Good class User { } class UserRepository { save() { } } class UserView { render() { } } -
Tight coupling:
// โ Bad class Service { constructor() { this.db = new Database(); } } // โ Good class Service { constructor(db) { this.db = db; } }
Summary
Architectural patterns organize applications. Key takeaways:
- MVC: Model, View, Controller separation
- MVVM: ViewModel for data binding
- MVP: Presenter handles logic
- Flux: Unidirectional data flow
- Clean Architecture: Layered design
- Improves maintainability
- Enables scalability
- Facilitates testing
Related Resources
- MVC Pattern - Wikipedia
- MVVM Pattern - Wikipedia
- Flux Architecture - Facebook
- Clean Architecture - Robert C. Martin
- Design Patterns
Next Steps
- Learn about Module Patterns
- Explore SOLID Principles
- Study Dependency Injection
- Practice architectural patterns
- Build scalable applications
Comments