Demystifying Architectural Patterns with JavaScript
Tags: javascript, python, webdev, programming
Published: 2023-10-24 00:20:04

**Howdy Folks!** Welcome to the world of architectural patterns! These design blueprints are the secret sauce behind crafting scalable, maintainable, and efficient software systems. Whether you're a fledgling developer or a seasoned pro, understanding these architectural patterns can significantly enhance your software engineering prowess.

In this article, we will delve into a variety of architectural patterns, using JavaScript with TypeScript to exemplify each one. Fear not, for we'll keep things as clear as a bell, ensuring even those with basic programming knowledge can appreciate these concepts.

## 1. Static Content Hosting Pattern
 
The Static Content Hosting Pattern is all about serving static assets like HTML, CSS, and JavaScript from a Content Delivery Network (CDN) to reduce server load and improve performance.
 
```typescript
// Load a static image from a CDN
const imageUrl = "https://alvisonhunter.com/imgs/codecrafterslabslogo.png";
const imgElement = document.createElement("img");
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
```

## 2. Event-Driven Pattern
 
The Event-Driven Pattern revolves around event handling. It decouples components by allowing them to communicate through events, promoting scalability and maintainability.
 
```typescript
// Event-driven example using the DOM
const button = document.getElementById("myButton");
button.addEventListener("click", (event) => {
  console.log("Button clicked!");
});
```

## 3. Peer-to-Peer Pattern
 
In the Peer-to-Peer Pattern, nodes or peers communicate directly with each other, without a central server. It's perfect for applications like file sharing.
 
```typescript
// Simulating peer-to-peer file sharing
class Peer {
  constructor(public name: string) {}
  sendFileTo(peer: Peer, file: string) {
    console.log(`${this.name} is sending ${file} to ${peer.name}`);
  }
}

const alvison = new Peer("Alvison");
const declan = new Peer("Declan");

alvison.sendFileTo(declan, "pythonCode.txt");
```

## 4. Publisher-Subscriber Pattern

The Publisher-Subscriber Pattern enables components to subscribe to and receive updates from publishers, enhancing modularity and reducing dependencies.
 
```typescript
class Publisher {
  private subscribers: Function[] = [];

  subscribe(subscriber: Function) {
    this.subscribers.push(subscriber);
  }

  publish(message: string) {
    this.subscribers.forEach((subscriber) => subscriber(message));
  }
}

const myPublisher = new Publisher();

function mySubscriber(message: string) {
  console.log(`Received: ${message}`);
}

myPublisher.subscribe(mySubscriber);
myPublisher.publish("CCL really rocks!");
```

## 5. Sharding Pattern
 
Sharding involves breaking a dataset into smaller, more manageable pieces that can be distributed across multiple servers or databases for improved performance.
 
```typescript
// Sharding users into different databases
const userShard1 = new Map<string, string>();
const userShard2 = new Map<string, string>();

function addUser(userId: string, data: string) {
  const shard = userId.charCodeAt(0) % 2 === 0 ? userShard1 : userShard2;
  shard.set(userId, data);
  return shard.get(userId);
}

const res = addUser("aHunter2023", "Frontend Web Developer");
console.log(res)
```

## 6. Circuit Breaker Pattern

The Circuit Breaker Pattern prevents a system from repeatedly trying to perform an operation that is likely to fail, preserving system integrity.
 
```typescript
class CircuitBreaker {
  private isOpen = false;

  attempt(operation: Function) {
    if (this.isOpen) {
      console.log("Circuit is open. Operation not attempted.");
    } else {
      try {
        operation();
      } catch (error) {
        console.error("Operation failed.", error);
        this.isOpen = true;
      }
    }
  }

  reset() {
    this.isOpen = false;
  }
}

const breaker = new CircuitBreaker();
breaker.attempt(() => {
  console.log("Risky operation here, anything from nacatamal to cacao")
});
```

## 7. Pipe-Filter Pattern

The Pipe-Filter pattern is like assembling a pipeline of filters to process and transform data in stages. It's an excellent choice for data manipulation tasks.

```typescript
interface Filter {
  process(data: string): string;
}

class FilterPipeline {
  filters: Filter[] = [];

  addFilter(filter: Filter) {
    this.filters.push(filter);
  }

  process(data: string) {
    for (const filter of this.filters) {
      data = filter.process(data);
    }
    return data;
  }
}

class UppercaseFilter implements Filter {
  process(data: string) {
    return data.toUpperCase();
  }
}

class ReverseFilter implements Filter {
  process(data: string) {
    return data.split('').reverse().join('');
  }
}

const pipeline = new FilterPipeline();
pipeline.addFilter(new UppercaseFilter());
pipeline.addFilter(new ReverseFilter());

const result = pipeline.process('Alvison');
console.log(result); // Output should be 'NOSIVLA'
```

## 8. Primary-Replica Pattern

The Primary-Replica pattern deals with data replication and distribution. It allows for load balancing and fault tolerance, where multiple replicas of the same data are maintained.

```typescript
class PrimaryReplicaManager {
  primary: string;
  replicas: string[] = [];

  setPrimary(data: string) {
    this.primary = data;
  }

  addReplica(data: string) {
    this.replicas.push(data);
  }

  getPrimary() {
    return this.primary;
  }

  getReplicas() {
    return this.replicas;
  }
}

const manager = new PrimaryReplicaManager();
manager.setPrimary('Primary Data');
manager.addReplica('Replica 1');
manager.addReplica('Replica 2');
manager.addReplica('Replica 3');

console.log(manager.getPrimary()); 
console.log(manager.getReplicas());
```

## 9. Model-View-Controller Pattern

The Model-View-Controller (MVC) pattern separates the application into three interconnected components: Model (data), View (user interface), and Controller (business logic). This separation enhances maintainability and testability.


```typescript
class Model {
  data: string;

  constructor() {
    this.data = [];
  }

  addData(item: string) {
    this.data.push(item);
  }
}

class View {
  render(data: string) {
    console.log(`${data.length} Rendered View:`, data);
  }
}

class Controller {
  model: Model;
  view: View;

  constructor(model: Model, view: View) {
    this.model = model;
    this.view = view;
  }

  update(data: string) {
    this.model.addData(data);
    this.view.render(this.model.data);
  }
}

const model = new Model();
const view = new View();
const controller = new Controller(model, view);

controller.update('Alvison Hunter');
controller.update('Declan Hunter');
controller.update('Liam Hunter');
```
**10. Interpreter Pattern**

The **Interpreter Pattern** is like having a multilingual guide in a foreign land. It helps you parse and interpret a language's grammar or expression and execute it. This is particularly useful when dealing with domain-specific languages.

```typescript
interface Interpreter {
  interpret(expression: string): number;
}

class SimpleInterpreter implements Interpreter {
  interpret(expression: string): number {
    const tokens = expression.split(' ');
   expression.split(" ").forEach((element, i) => {
  if (i > 0) {
    tokens[i] = parseInt(element);
  }
});

let result = 0;

if (tokens[0] === "add") {
  result = tokens[1] + tokens[2];
} else if (tokens[0] === "subtract") {
  result = tokens[1] - tokens[2];
} else if (tokens[0] === "multiply") {
  result = tokens[1] * tokens[2];
} else if (tokens[0] === "divide") {
  result = tokens[1] / tokens[2];
}
    return result;
  }
}

const interpreter = new SimpleInterpreter();
console.log('Interpreter Pattern Result:');
console.log('Addition:', interpreter.interpret('add 10 2')); // outputs 12
console.log('Subtraction:', interpreter.interpret('subtract 30 20')); //outputs 10
console.log('Multiplication:', interpreter.interpret('multiply 10 2')); // outputs 20
console.log('Division:', interpreter.interpret('divide 10 2')); // outputs 5
```

**11. Client-Server Pattern**

The **Client-Server Pattern** is akin to the classic waiter-customer relationship in a restaurant. The server provides services, and clients consume them. This architecture separates concerns, improving scalability and maintenance.

```typescript
// Server
class Server {
  handleRequest(request: string): string {
    return `Received and processed: ${request}`;
  }
}

// Client
class Client {
  sendRequest(request: string, server: Server): string {
    return server.handleRequest(request);
  }
}

const server = new Server();
const client = new Client();

const response = client.sendRequest('Serve me, Server!', server);
console.log('Client-Server Pattern Response:', response);
```

**12. Layered Pattern**

The **Layered Pattern** is analogous to assembling a delicious sandwich. It organizes your application into layers, each handling specific tasks, making it easier to maintain and scale your code.

```typescript
// Presentation Layer
class PresentationLayer {
  render(data: string): string {
    return `Rendered: ${data}`;
  }
}

// Business Logic Layer
class BusinessLogicLayer {
  process(data: string): string {
    return `Processed: ${data}`;
  }
}

// Data Access Layer
class DataAccessLayer {
  retrieveData(): string {
    return 'Data from the database';
  }
}

// Main Application
const presentationLayer = new PresentationLayer();
const businessLayer = new BusinessLogicLayer();
const dataAccessLayer = new DataAccessLayer();

const data = dataAccessLayer.retrieveData();
const processedData = businessLayer.process(data);
const renderedData = presentationLayer.render(processedData);

console.log('Layered Pattern Result:', renderedData);
```

**13. Microservices Pattern**

The **Microservices Pattern** is like having a buffet of small dishes at a grand feast. It breaks down a monolithic application into smaller, independent services that can be developed and deployed separately.

Here's a simple JavaScript TypeScript example using HTTP communication:

```typescript
// Microservice 1
const microservice1 = () => {
  return 'Microservice 1 is serving.';
};

// Microservice 2
const microservice2 = () => {
  return 'Microservice 2 is serving.';
};

console.log('Microservices Pattern Result 1:', microservice1());
console.log('Microservices Pattern Result 2:', microservice2());
```

**14. Command Query Responsibility Segregation Pattern**

The **Command Query Responsibility Segregation (CQRS) Pattern** is like separating the roles of chefs and waitstaff in a restaurant. It segregates the reading (query) and writing (command) operations, optimizing your system for performance and scalability.

Here's a straightforward JavaScript TypeScript example:

```typescript
class OrderService {
  private orders: string[] = [];

  addOrder(order: string): void {
    this.orders.push(order);
  }

  getOrders(): string[] {
    return this.orders;
  }
}

const orderService = new OrderService();
orderService.addOrder('Order 1');
orderService.addOrder('Order 2');
const orders = orderService.getOrders();

console.log('CQRS Pattern Orders:', orders);
```

In conclusion, understanding these architectural patterns is essential for architects and developers alike. They serve as fundamental tools in your software design toolbox, helping you create more efficient, maintainable, and scalable applications. By using JavaScript with TypeScript, we've made these concepts approachable, even for those with basic programming knowledge.

Remember, it's not just about the code; it's about creating resilient, scalable, and maintainable systems. So go ahead, explore these patterns, and start building better software today! Happy coding!

❤️ Enjoyed the article? Your feedback fuels more content.
💬 Share your thoughts in a comment.
🔖 No time to read now? Well, Bookmark for later.
🔗 If it helped, pass it on, dude!