Lädt...


🔧 Applying SOLID Principles in JavaScript and TypeScript Framework


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

Introduction

The SOLID principles form the foundation of clean, scalable, and maintainable software development. Though these principles originated in Object-Oriented Programming (OOP), they can be effectively applied in JavaScript (JS) and TypeScript (TS) frameworks like React and Angular. This article explains each principle with real-life examples in both JS and TS.

1. Single Responsibility Principle (SRP)

Principle: A class or module should have only one reason to change. It should be responsible for a single piece of functionality.

  • Example in JavaScript (React):

In React, we often see components responsible for too many things—such as managing both UI and business logic.

Anti-pattern:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData();
  }, [userId]);

  async function fetchUserData() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
  }

  return <div>{user?.name}</div>;
}

Here, the UserProfile component violates SRP because it handles both UI rendering and data fetching.

Refactor:

// Custom hook for fetching user data
function useUserData(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    async function fetchUserData() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data);
    }
    fetchUserData();
  }, [userId]);

  return user;
}

// UI Component
function UserProfile({ userId }) {
  const user = useUserData(userId); // Moved data fetching logic to a hook

  return <div>{user?.name}</div>;
}

By using a custom hook (useUserData), we separate the data-fetching logic from the UI, keeping each part responsible for a single task.

  • Example in TypeScript (Angular):

In Angular, services and components can become cluttered with multiple responsibilities.

Anti-pattern:

@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }

  updateUserProfile(userId: string, data: any) {
    // Updating the profile and handling notifications
    return this.http.put(`/api/users/${userId}`, data).subscribe(() => {
      console.log('User updated');
      alert('Profile updated successfully');
    });
  }
}

This UserService has multiple responsibilities: fetching, updating, and handling notifications.

Refactor:


@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }

  updateUserProfile(userId: string, data: any) {
    return this.http.put(`/api/users/${userId}`, data);
  }
}

// Separate notification service
@Injectable()
export class NotificationService {
  notify(message: string) {
    alert(message);
  }
}

By splitting the notification handling into a separate service (NotificationService), we ensure that each class has a single responsibility.

2. Open/Closed Principle (OCP)

Principle: Software entities should be open for extension but closed for modification. This means that you should be able to extend the behavior of a module without altering its source code.

  • Example in JavaScript (React):

You might have a form validation function that works well but could require additional validation logic in the future.

Anti-pattern:

function validate(input) {
  if (input.length < 5) {
    return 'Input is too short';
  }
  if (!input.includes('@')) {
    return 'Invalid email';
  }
  return 'Valid input';
}

Whenever you need a new validation rule, you'd have to modify this function, violating OCP.

Refactor:

function validate(input, rules) {
  return rules.map(rule => rule(input)).find(result => result !== 'Valid') || 'Valid input';
}

const lengthRule = input => input.length >= 5 ? 'Valid' : 'Input is too short';
const emailRule = input => input.includes('@') ? 'Valid' : 'Invalid email';

validate('[email protected]', [lengthRule, emailRule]);

Now, we can extend validation rules without modifying the original validate function, adhering to OCP.

  • Example in TypeScript (Angular):

In Angular, services and components should be designed to allow new features to be added without modifying the core logic.

Anti-pattern:

export class NotificationService {
  send(type: 'email' | 'sms', message: string) {
    if (type === 'email') {
      // Send email
    } else if (type === 'sms') {
      // Send SMS
    }
  }
}

This service violates OCP since you'd need to modify the send method every time you add a new notification type (e.g., push notifications).

Refactor:

interface Notification {
  send(message: string): void;
}

@Injectable()
export class EmailNotification implements Notification {
  send(message: string) {
    // Send email logic
  }
}

@Injectable()
export class SMSNotification implements Notification {
  send(message: string) {
    // Send SMS logic
  }
}

@Injectable()
export class NotificationService {
  constructor(private notifications: Notification[]) {}

  notify(message: string) {
    this.notifications.forEach(n => n.send(message));
  }
}

Now, adding new notification types only requires creating new classes without changing the NotificationService itself.

3. Liskov Substitution Principle (LSP)

Principle: Subtypes must be substitutable for their base types. Derived classes or components should be able to replace base classes without affecting the correctness of the program.

  • Example in JavaScript (React):

When using higher-order components (HOCs) or rendering different components conditionally, LSP helps ensure that all components behave predictably.

Anti-pattern:

function Button({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

function LinkButton({ href }) {
  return <a href={href}>Click me</a>;
}

<Button onClick={() => {}} />;
<LinkButton href="/home" />;

Here, Button and LinkButton are inconsistent. One uses onClick, and the other uses href, making substitution difficult.

Refactor:

function Clickable({ children, onClick }) {
  return <div onClick={onClick}>{children}</div>;
}

function Button({ onClick }) {
  return <Clickable onClick={onClick}>
    <button>Click me</button>
  </Clickable>;
}

function LinkButton({ href }) {
  return <Clickable onClick={() => window.location.href = href}>
    <a href={href}>Click me</a>
  </Clickable>;
}

Now, both Button and LinkButton behave similarly, adhering to LSP.

  • Example in TypeScript (Angular):

Anti-pattern:

class Rectangle {
  constructor(protected width: number, protected height: number) {}

  area() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  constructor(size: number) {
    super(size, size);
  }

  setWidth(width: number) {
    this.width = width;
    this.height = width; // Breaks LSP
  }
}

Modifying setWidth in Square violates LSP because Square behaves differently from Rectangle.

Refactor:

class Shape {
  area(): number {
    throw new Error('Method not implemented');
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }

  area() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(private size: number) {
    super();
  }

  area() {
    return this.size * this.size;
  }
}

Now, Square and Rectangle can be substituted without violating LSP.

4. Interface Segregation Principle (ISP):

Principle: Clients should not be forced to depend on interfaces they do not use.

  • Example in JavaScript (React):

React components sometimes receive unnecessary props, leading to tightly coupled and bulky code.

Anti-pattern:

function MultiPurposeComponent({ user, posts, comments }) {
  return (
    <div>
      <UserProfile user={user} />
      <UserPosts posts={posts} />
      <UserComments comments={comments} />
    </div>
  );
}

Here, the component depends on multiple props, even though it might not always use them.

Refactor:

function UserProfileComponent({ user }) {
  return <UserProfile user={user} />;
}

function UserPostsComponent({ posts }) {
  return <UserPosts posts={posts} />;
}

function UserCommentsComponent({ comments }) {
  return <UserComments comments={comments} />;
}

By splitting the component into smaller ones, each only depends on the data it actually uses.

  • Example in TypeScript (Angular):

Anti-pattern:

interface Worker {
  work(): void;
  eat(): void;
}

class HumanWorker implements Worker {
  work() {
    console.log('Working');
  }
  eat() {
    console.log('Eating');
  }
}

class RobotWorker implements Worker {
  work() {
    console.log('Working');
  }
  eat() {
    throw new Error('Robots do not eat'); // Violates ISP
  }
}

Here, RobotWorker is forced to implement an irrelevant eat method.

Refactor:

interface Worker {
  work(): void;
}

interface Eater {
  eat(): void;
}

class HumanWorker implements Worker, Eater {
  work() {
    console.log('Working');
  }

  eat() {
    console.log('Eating');
  }
}

class RobotWorker implements Worker {
  work() {
    console.log('Working');
  }
}

By separating Worker and Eater interfaces, we ensure that clients only depend on what they need.

5. Dependency Inversion Principle (DIP):

Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).

  • Example in JavaScript (React):

Anti-pattern:

function fetchUser(userId) {
  return fetch(`/api/users/${userId}`).then(res => res.json());
}

function UserComponent({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user?.name}</div>;
}

Here, UserComponent is tightly coupled with the fetchUser function.

Refactor:

function UserComponent({ userId, fetchUserData }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUserData(userId).then(setUser);
  }, [userId, fetchUserData]);

  return <div>{user?.name}</div>;
}

// Usage
<UserComponent userId={1} fetchUserData={fetchUser} />;

By injecting fetchUserData into the component, we can easily swap out the implementation for testing or different use cases.

  • Example in TypeScript (Angular):

Anti-pattern:

@Injectable()
export class UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get(`/api/users/${userId}`);
  }
}

@Injectable()
export class UserComponent {
  constructor(private userService: UserService) {}

  loadUser(userId: string) {
    this.userService.getUser(userId).subscribe(user => console.log(user));
  }
}

UserComponent is tightly coupled with UserService, making it hard to swap out UserService.

Refactor:

interface UserService {
  getUser(userId: string): Observable<User>;
}

@Injectable()
export class ApiUserService implements UserService {
  constructor(private http: HttpClient) {}

  getUser(userId: string) {
    return this.http.get<User>(`/api/users/${userId}`);
  }
}

@Injectable()
export class UserComponent {
  constructor(private userService: UserService) {}

  loadUser(userId: string) {
    this.userService.getUser(userId).subscribe(user => console.log(user));
  }
}

By depending on an interface (UserService), UserComponent is now decoupled from the concrete implementation of ApiUserService.

Next Steps

Whether you're working on the front end with frameworks like React or Angular, or on the back end with Node.js, the SOLID principles serve as a guide to ensure that your software architecture remains solid.

To fully integrate these principles into your projects:

  • Practice regularly: Refactor existing codebases to apply SOLID principles and review code for adherence.
  • Collaborate with your team: Encourage best practices through code reviews and discussions around clean architecture.
  • Stay curious: SOLID principles are just the beginning. Explore other architectural patterns like MVC, MVVM, or CQRS that build on these fundamentals to further improve your designs.

Conclusion

The SOLID principles are highly effective for ensuring that your code is clean, maintainable, and scalable, even in JavaScript and TypeScript frameworks like React and Angular. Applying these principles enables developers to write flexible and reusable code that’s easy to extend and refactor as requirements evolve. By following SOLID, you can make your codebase robust and ready for future growth.

...

🔧 Applying SOLID Principles in JavaScript and TypeScript Framework


📈 62.5 Punkte
🔧 Programmierung

🔧 Crafting Maintainable and Scalable Software: Applying SOLID Principles


📈 41.51 Punkte
🔧 Programmierung

🔧 Mastering Code Quality: Applying SOLID Principles in a Library Checkout System


📈 40.21 Punkte
🔧 Programmierung

🔧 SOLID Principles / Open - closed principles -


📈 36.16 Punkte
🔧 Programmierung

🔧 SOLID Principles Aren't Principles


📈 36.16 Punkte
🔧 Programmierung

🔧 SOLID Principles: They're Rock-Solid for Good Reason!


📈 35.74 Punkte
🔧 Programmierung

🔧 TypeScript Syntax Rules: Mastering TypeScript by Applying These Rules


📈 34.45 Punkte
🔧 Programmierung

🔧 Why you should learn TypeScript and ditch JavaScript? TypeScript vs JavaScript


📈 30.86 Punkte
🔧 Programmierung

🔧 What are solid principles in JavaScript ?


📈 29.64 Punkte
🔧 Programmierung

🔧 Making SOLID Simple: A JavaScript Guide to Clean Code Principles


📈 29.64 Punkte
🔧 Programmierung

🔧 🛠️ Cracking the Code: Master SOLID Principles in JavaScript with Real-World Examples 🚀


📈 29.64 Punkte
🔧 Programmierung

🔧 Mastering SOLID Principles: A Guide with JavaScript Examples


📈 29.64 Punkte
🔧 Programmierung

🔧 TypeScript ✔ vs JavaScript ❌ : How TypeScript Outshines JavaScript


📈 29.55 Punkte
🔧 Programmierung

🔧 TypeScript ✔ vs JavaScript ❌ : How TypeScript Outshines JavaScript


📈 29.55 Punkte
🔧 Programmierung

🔧 TypeScript ✔ vs JavaScript ❌ : How TypeScript Outshines JavaScript


📈 29.55 Punkte
🔧 Programmierung

📰 Applying the Principles of Quantum Entanglement to Secure Communication


📈 28.43 Punkte
📰 IT Security Nachrichten

🎥 Keynote: Applying Security Engineering Principles to Complex Composite Systems - Neal Ziring


📈 28.43 Punkte
🎥 IT Security Video

🐧 Applying the Principles of Zero Trust to SSH


📈 28.43 Punkte
🐧 Linux Tipps

📰 Applying the Principles of Zero Trust to SSH


📈 28.43 Punkte
📰 IT Security Nachrichten

📰 Applying Shift Left principles to third party risk management


📈 28.43 Punkte
📰 IT Security Nachrichten

🔧 Improve Microservices Security by Applying Zero-Trust Principles


📈 28.43 Punkte
🔧 Programmierung

🔧 Connecting With Users: Applying Principles Of Communication To UX Research


📈 28.43 Punkte
🔧 Programmierung

📰 Applying DevSecOps principles to machine learning workloads


📈 28.43 Punkte
📰 IT Security Nachrichten

🔧 S.O.L.I.D. Principles: Applying Single Responsibility Principle to Real-World Code


📈 28.43 Punkte
🔧 Programmierung

🔧 Applying the Open/Closed Principle in TypeScript: A Concise Overview


📈 25.34 Punkte
🔧 Programmierung

🔧 SOLID, KISS, YAGNI and DRY Principles


📈 25.28 Punkte
🔧 Programmierung

🔧 Mastering SOLID Principles in C# Building Robust and Maintainable Software


📈 25.28 Punkte
🔧 Programmierung

🔧 Understanding SOLID Principles and Their Implementation in React


📈 25.28 Punkte
🔧 Programmierung

🔧 SOLID Design Principles and Design Patterns Crash Course


📈 25.28 Punkte
🔧 Programmierung

🔧 How to leverage polymorphism to design flexible and maintainable software that adheres to SOLID principles


📈 25.28 Punkte
🔧 Programmierung

🔧 Mastering SOLID Principles in React Native and MERN Stack


📈 25.28 Punkte
🔧 Programmierung

🔧 Enhancing .NET Applications with CQRS and SOLID Principles


📈 25.28 Punkte
🔧 Programmierung

matomo