DEV Community

Abanoub Kerols
Abanoub Kerols

Posted on

Advanced SOLID Principles in a Blogging Platform with JavaScript

The SOLID principles—Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—are essential for crafting maintainable and scalable object-oriented code. This article dives deeper into applying these principles in a blogging platform, using modern JavaScript features like async/await and ES modules. We’ll explore complex, real-world scenarios, such as managing blog posts, user authentication, and comment moderation, with asynchronous operations.

1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, focusing on a single responsibility.

Real-World Scenario: Blog Post Management

In a blogging platform, a BlogPost class might handle multiple tasks: creating posts, publishing them, and sending notifications.

Bad Example
A class with multiple responsibilities:

//blog-post.js
export class BlogPost {
  constructor(title, content, author) {
    this.title = title;
    this.content = content;
    this.author = author;
  }

  async saveToDatabase() {
    console.log(`Saving post "${this.title}" to database`);
    // Simulate async DB operation
    await new Promise(resolve => setTimeout(resolve, 1000));
  }

  async publish() {
    console.log(`Publishing post "${this.title}"`);
    // Simulate async publishing
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  async notifySubscribers() {
    console.log(`Notifying subscribers about "${this.title}"`);
    // Simulate async notification
    await new Promise(resolve => setTimeout(resolve, 300));
  }
}

(async () => {
  const post = new BlogPost("SOLID Principles", "Content...", "Alice");
  await post.saveToDatabase();
  await post.publish();
  await post.notifySubscribers();
})();
Enter fullscreen mode Exit fullscreen mode

The BlogPost class handles saving, publishing, and notifications, violating SRP.

Good Example
Separate responsibilities into distinct classes:

// blog-post.js
export class BlogPost {
  constructor(title, content, author) {
    this.title = title;
    this.content = content;
    this.author = author;
  }

  getDetails() {
    return { title: this.title, content: this.content, author: this.author };
  }
}

// post-repository.js
export class PostRepository {
  async save(post) {
    console.log(`Saving post "${post.getDetails().title}" to database`);
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
}

// post-publisher.js
export class PostPublisher {
  async publish(post) {
    console.log(`Publishing post "${post.getDetails().title}"`);
    await new Promise(resolve => setTimeout(resolve, 500));
  }
}

// notification-service.js
export class NotificationService {
  async notify(post) {
    console.log(`Notifying subscribers about "${post.getDetails().title}"`);
    await new Promise(resolve => setTimeout(resolve, 300));
  }
}

// main.js
import { BlogPost } from './blog-post.js';
import { PostRepository } from './post-repository.js';
import { PostPublisher } from './post-publisher.js';
import { NotificationService } from './notification-service.js';

(async () => {
  const post = new BlogPost("SOLID Principles", "Content...", "Alice");
  const repository = new PostRepository();
  const publisher = new PostPublisher();
  const notifier = new NotificationService();

  await repository.save(post);
  await publisher.publish(post);
  await notifier.notify(post);
})();
Enter fullscreen mode Exit fullscreen mode

Each class has a single responsibility, improving maintainability and testability.

2. Open/Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification.

Real-World Scenario: Comment Moderation

A blogging platform needs to moderate comments based on different policies (e.g., spam filtering, profanity checks).

Bad Example
Modifying the class for new moderation policies:

export class CommentModerator {
  moderate(comment, policy) {
    if (policy === "spam") {
      console.log(`Checking "${comment}" for spam`);
      return !comment.includes("http");
    } else if (policy === "profanity") {
      console.log(`Checking "${comment}" for profanity`);
      return !comment.includes("badword");
    }
    return true;
  }
}

const moderator = new CommentModerator();
console.log(moderator.moderate("Check this link: http://example.com", "spam")); // false
console.log(moderator.moderate("This is a badword comment", "profanity")); // false
Enter fullscreen mode Exit fullscreen mode

Adding a new policy (e.g., length check) requires modifying CommentModerator.

Good Example
Use a strategy pattern for extensibility:

export class CommentModerator {
  moderate(comment, policy) {
    return policy.apply(comment);
  }
}

export class SpamPolicy {
  apply(comment) {
    console.log(`Checking "${comment}" for spam`);
    return !comment.includes("http");
  }
}

export class ProfanityPolicy {
  apply(comment) {
    console.log(`Checking "${comment}" for profanity`);
    return !comment.includes("badword");
  }
}

const moderator = new CommentModerator();
const spamPolicy = new SpamPolicy();
const profanityPolicy = new ProfanityPolicy();

console.log(moderator.moderate("Check this link: http://example.com", spamPolicy)); // false
console.log(moderator.moderate("This is a badword comment", profanityPolicy)); // false

Enter fullscreen mode Exit fullscreen mode

New policies can be added by creating new classes, adhering to OCP.

3. Liskov Substitution Principle (LSP)
Definition: Subclasses should be substitutable for their base classes without breaking functionality.

Real-World Scenario: Content Types

A blogging platform supports different content types (e.g., articles, videos) that need rendering.

Bad Example
A subclass that breaks substitution:

export class Content {
  render() {
    console.log("Rendering content in HTML");
  }
}

export class VideoContent extends Content {
  render() {
    throw new Error("Video content requires a player, not HTML!");
  }
}

function displayContent(content) {
  content.render();
}

const article = new Content();
const video = new VideoContent();

displayContent(article); // Works
displayContent(video); // Throws error
Enter fullscreen mode Exit fullscreen mode

VideoContent cannot substitute Content due to the error.

Good Example
Use a more general abstraction:

export class Content {
  display() {
    console.log("Displaying content...");
  }
}

export class ArticleContent extends Content {
  display() {
    console.log("Rendering article in HTML");
  }
}

export class VideoContent extends Content {
  display() {
    console.log("Playing video in media player");
  }
}

function displayContent(content) {
  content.display();
}

const article = new ArticleContent();
const video = new VideoContent();

displayContent(article); // Rendering article in HTML
displayContent(video); // Playing video in media player
Enter fullscreen mode Exit fullscreen mode

Both subclasses can substitute Content seamlessly.

4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to implement interfaces they don’t need.

Real-World Scenario: User Authentication

Users may authenticate via different methods (e.g., email/password, OAuth), but not all methods require multi-factor authentication (MFA).

Bad Example
A single interface forces unnecessary implementation:

export class AuthMethod {
  login() {
    throw new Error("Method not implemented");
  }
  enableMFA() {
    throw new Error("Method not implemented");
  }
}

export class EmailAuth extends AuthMethod {
  login() {
    console.log("Logging in with email/password");
  }
  enableMFA() {
    console.log("Enabling MFA for email");
  }
}

export class OAuth extends AuthMethod {
  login() {
    console.log("Logging in with OAuth");
  }
  enableMFA() {
    throw new Error("OAuth doesn't support MFA!");
  }
}
Enter fullscreen mode Exit fullscreen mode

OAuth is forced to implement enableMFA, which it doesn’t need.

Good Example
Split interfaces:

export class Loginable {
  login() {
    throw new Error("Method not implemented");
  }
}

export class MFA {
  enableMFA() {
    throw new Error("Method not implemented");
  }
}

export class EmailAuth extends Loginable {
  login() {
    console.log("Logging in with email/password");
  }

  enableMFA() {
    console.log("Enabling MFA for email");
  }
}

export class OAuth extends Loginable {
  login() {
    console.log("Logging in with OAuth");
  }
}

const emailAuth = new EmailAuth();
const oauth = new OAuth();

emailAuth.login(); // Logging in with email/password
emailAuth.enableMFA(); // Enabling MFA for email
oauth.login(); // Logging in with OAuth
Enter fullscreen mode Exit fullscreen mode

OAuth only implements Loginable, adhering to ISP.

5. Dependency Inversion Principle (DIP)
Definition: High-level modules should depend on abstractions, not concrete implementations.

Real-World Scenario: Analytics Tracking

A blogging platform tracks user interactions (e.g., page views) using different analytics providers.

Bad Example
Direct dependency on a concrete class:

export class GoogleAnalytics {
  async track(event) {
    console.log(`Tracking "${event}" with Google Analytics`);
    await new Promise(resolve => setTimeout(resolve, 200));
  }
}

export class AnalyticsService {
  constructor() {
    this.tracker = new GoogleAnalytics();
  }

  async logEvent(event) {
    await this.tracker.track(event);
  }
}

(async () => {
  const analytics = new AnalyticsService();
  await analytics.logEvent("Page View");
})();
Enter fullscreen mode Exit fullscreen mode

AnalyticsService is tightly coupled to GoogleAnalytics.

Good Example
Depend on an abstraction:

export class AnalyticsProvider {
  async track(event) {
    throw new Error("Method not implemented");
  }
}

export class GoogleAnalytics extends AnalyticsProvider {
  async track(event) {
    console.log(`Tracking "${event}" with Google Analytics`);
    await new Promise(resolve => setTimeout(resolve, 200));
  }
}

export class MixpanelAnalytics extends AnalyticsProvider {
  async track(event) {
    console.log(`Tracking "${event}" with Mixpanel`);
    await new Promise(resolve => setTimeout(resolve, 200));
  }
}

export class AnalyticsService {
  constructor(provider) {
    this.tracker = provider;
  }

  async logEvent(event) {
    await this.tracker.track(event);
  }
}

(async () => {
  const google = new GoogleAnalytics();
  const mixpanel = new MixpanelAnalytics();

  const analytics1 = new AnalyticsService(google);
  const analytics2 = new AnalyticsService(mixpanel);

  await analytics1.logEvent("Page View"); // Tracking with Google Analytics
  await analytics2.logEvent("Page View"); // Tracking with Mixpanel
})();
Enter fullscreen mode Exit fullscreen mode

AnalyticsService depends on the AnalyticsProvider abstraction, enabling flexibility.

Conclusion

Applying SOLID principles in a blogging platform using modern JavaScript features results in modular, maintainable, and scalable code. By adhering to SRP, OCP, LSP, ISP, and DIP, developers can handle complex requirements like asynchronous operations and diverse integrations with ease.Use these principles to build robust applications that adapt to changing requirements.

Top comments (0)