Logo

dev-resources.site

for different kinds of informations.

SOLID Principles in Functional Programming (FP) with examples

Published at
10/4/2024
Categories
solid
principles
functional
typescript
Author
rosselli00
Author
10 person written this
rosselli00
open
SOLID Principles in Functional Programming (FP) with examples

The SOLID principles are five design principles that aim to create software easy-to-maintain, flexible, and scalable. They were introduced by Robert C. Martin (Uncle Bob) and are widely adopted in software development.

While the SOLID principles are traditionally applied to Object-Oriented Programming (OOP), they can also be adapted to Functional Programming (FP). Here's an overview of each SOLID principle with real-world examples in Typescript using functional programming concepts.

1. Single Responsibility Principle (SRP)

A function should have one, and only one, reason to change. It should do one thing (responsibility) and do it well.

In FP, functions should focus on solving a single problem or performing one task. By adhering to SRP, you can easily reason about code, test it, and change individual behaviours without affecting other parts of the system.

When to use: Whenever you need to ensure modularity and separation of concerns. It’s easier to maintain, test, and refactor.

When to refactor:

  • When a function has multiple unrelated responsibilities.
  • When a function is becoming too large and complex.
  • When a function is difficult to understand or test.

Example: Let’s say we have an app that handles user registration, validation, and email sending. Each function should focus on one task:

const validateEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

const sendEmail = (email: string, content: string): void => {/* logic to send email */};

const registerUser = (email: string, password: string): void => {
  if (!validateEmail(email)) throw new Error('Invalid email');
  // logic to register user
  sendEmail(email, 'Welcome to the platform!');
};
Enter fullscreen mode Exit fullscreen mode

2. Open/Closed Principle (OCP)

Software entities (functions, modules, etc.) should be open for extension but closed for modification. This means you can add new functionality without changing existing code.

You should build functions in a way that new behavior can be added without changing existing code. In FP, higher-order functions and function composition can help achieve this.

When to use: When you want to add new features (e.g., different notification methods) without modifying the core logic.

When to refactor:

  • When you need to add new functionality to an existing system without modifying the existing code.
  • When you want to make your code more extensible and maintainable.

Example: Suppose we want to extend our user registration system to support different types of notifications (email, SMS).

type Notify = (message: string) => void;

const emailNotify: Notify = (message) => {/* send email logic */ };
const smsNotify: Notify = (message) => {/* send SMS logic */ };

const registerUser = (email: string, password: string, notify: Notify): void => {
  if (!validateEmail(email)) throw new Error('Invalid email');
  // logic to register user
  notify('Welcome to the platform!');
};

// extend functionality without modifying registerUser
registerUser('[email protected]', 'password123', emailNotify);
registerUser('[email protected]', 'password123', smsNotify);
Enter fullscreen mode Exit fullscreen mode

3. Liskov Substitution Principle (LSP)

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program.

In FP, this translates to ensuring that different implementations of a function or data transformation behave consistently. If you replace one function with another that has the same signature, the program should work as expected.

When to use (or refactor): When you need interchangeable behaviors or formatters. It helps keep your code flexible and open to new use cases.

Example: Let’s assume we have a function that formats user data for export in different formats (JSON, CSV).

type Formatter = (data: any) => string;

const jsonFormatter: Formatter = (data) => JSON.stringify(data);
const csvFormatter: Formatter = (data) => {
  // logic to convert data to CSV
  return "CSV format";
};

const exportUserData = (data: any, formatter: Formatter): string => formatter(data);

console.log(exportUserData({ name: 'John Doe' }, jsonFormatter)); // JSON output
console.log(exportUserData({ name: 'John Doe' }, csvFormatter)); // CSV output
Enter fullscreen mode Exit fullscreen mode

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. Instead of one large interface, multiple smaller interfaces are preferred.

This principle suggests that functions should only accept the parameters they need. In FP, this can be thought of as creating smaller, more specific functions rather than large ones that do too much or rely on too many inputs.

When to use: When you want to avoid functions taking large input structures or unnecessary dependencies.

When to refactor:

  • When you have a large interface that is used by multiple clients.
  • When you want to create more reusable and modular components.

Example:
Creating smaller, focused functions instead of large interfaces allows clients to only depend on what they need.

const sendMessage = (message: string): void => {
    // Logic to send message
    console.log(`Message sent: ${message}`);
};

const receiveMessage = (): string => {
    // Logic to receive message
    const message = "Hello!";
    console.log(`Message received: ${message}`);
    return message;
};

// each function serves a specific purpose without unnecessary dependencies.
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

In FP, this can be applied by having high-level functions depend on abstractions (like higher-order functions) rather than concrete implementations. We decouple the implementation of a function from its usage by passing behaviour as a function.

When to use: When you want to keep your code loosely coupled, allowing easy swapping of different behaviours without changing the high-level logic, enhancing flexibility and testability.

When to refactor:

  • When you want to decouple high-level modules from low-level modules.
  • When you want to make your code more flexible and testable.

Example: Using higher-order functions to inject dependencies allows us to decouple high-level logic from low-level implementations.

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

const notifyUser = (notificationSender: NotificationSender, message: string): void => notificationSender.send(message);

// Low-level module implementation
const emailSender: NotificationSender = {
    send(message) { console.log(`Email sent with message: ${message}`) }
};

// High-level module using dependency injection
notifyUser(emailSender, "Notification sent!");
Enter fullscreen mode Exit fullscreen mode

Example: Suppose we want a notification system that can support multiple notification methods (email, SMS, etc.) but without hardcoding them.

type Notify = (message: string) => void;

const notifyUser = (message: string, notify: Notify): void => notify(message);

const emailNotify: Notify = (message) => {/* send email logic */ };
const smsNotify: Notify = (message) => {/* send SMS logic */ };

// High-level notifyUser depends on the abstraction (Notify), not the concrete implementation
notifyUser('Welcome!', emailNotify);
notifyUser('Your order is confirmed!', smsNotify);
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • SRP: Each function has a single, well-defined responsibility.
  • OCP: New functionality can be added by creating new functions without modifying existing ones.
  • LSP: Functions that take interfaces as arguments can be used with any implementation of those interfaces.
  • ISP: Functions are designed with small, focused interfaces.
  • DIP: Functions depend on abstractions (interfaces) rather than concrete implementations.
principles Article's
30 articles in total
Favicon
Tell Don't Ask principle explained
Favicon
THE M.O.I.S.T Principle
Favicon
Functional Programming (FP) Principles with examples
Favicon
SOLID Principles in Functional Programming (FP) with examples
Favicon
16 Principles for Tech-led Start-ups as a Software Engineer
Favicon
Applying the four principles of accessibility
Favicon
Mastering SOLID principles in Flutter
Favicon
We got 3 main principles for writing Technical Blog posts. Here’s what we learned.
Favicon
Testing Without the Tears: How Mocking Boundaries Can Strengthen Your Tests
Favicon
Advantages of Modularity: Simplified Development Process
Favicon
SOLID Principles: It's That Easy! 😱 STANDOUT 🌟 with SOLID Principles! πŸ§™β€β™‚οΈβœ¨
Favicon
10 principles of good web design.
Favicon
πŸ“œ Novu's Communication Manifest: Lighting the Path to our Future πŸ’‘
Favicon
2023 Industry Trends in Mobile Application User Interface
Favicon
Web Design Principles: From Layout to Navigation, Master the Basics
Favicon
Agile Software Development: Principles, Practices, and Benefits
Favicon
Considering Agile Principles from a different angle
Favicon
SOLID Principles in Swift: A Practical Guide
Favicon
Development principles you should know
Favicon
One-page Software design cheat sheet
Favicon
Top 10 trending github repos for Java developers in this weekπŸ‘‰.
Favicon
What You Need To Know About Design Rules And Principles
Favicon
Security
Favicon
Monitoring/Observability
Favicon
Design to encapsulate volatility
Favicon
Testing
Favicon
Code Clean & Simple
Favicon
Automation
Favicon
Tech Principles @ Coolblue
Favicon
Recovery over perfection

Featured ones: