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.
// 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.
// 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.
// 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.
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.
// 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.
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.
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.
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.
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.
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.
// 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.
// 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:
// 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:
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!
Top comments (0)