How To Use SOLID for Object-Oriented and Functional Programming

SOLID is an acronym that I’ll break down in this article that caters specifically to Object-Oriented Programming (OOP) paradigms. With Functional Programming (FP) also becoming more mainstream, I wanted to examine SOLID and flesh out the deeper meaning to apply it to both programming paradigms. Let’s explore SOLID for Object-Oriented and Functional Programming.

This article refers to concepts in OOP and FP paradigms but does not dive into details on them. While I want to cover these topics in their own articles. For this article, I’ll be sure to link to helpful wikis for further reading.

Defining SOLID for Object-Oriented Programming

  • S (Single-Responsibility Principle; SRP): This is, in my opinion, one of the most important rules and promotes keeping concerns separated. The idea is that each class should only have one responsibility.
  • O (Open-closed Principle; OCP): Open to extension but closed for modification. This especially applies to abstractions. The goal with this principle is to ensure those that inherit from the base will deal with complexity rather than need to modify the base. If you do have a base, it should be clean of all details from its children and as simple as possible.
  • L (Liskov Substitution Principle; LSP): Any given implementer of an interface or child of abstraction is “swappable” with any other from the same interface/abstraction. In other words: they must fulfill their contracts making the interface/abstraction a semantic agreement.
  • I (Interface Segregation Principle; ISP): Similar to SRP in some ways. ISP states that you should favor many interfaces with isolated concerns versus one interface with mixed concerns.
  • D (Dependency Inversion Principle; DIP): This principle states that in all possible cases, you should design your code around using interfaces or abstractions versus the concrete implementations. This will make the system more robust over time as you are coupling your code to work with specific concerns of an interface versus any particular implementation.

Benefits of SOLID

SOLID is fundamental for OOP and is absolutely necessary to make a maintainable OO codebase. I wanted to focus more on the high level of why SOLID is important. This Code Project article using C# goes into great detail with examples if you want to explore example code with OOP.

The reason why SOLID is so beneficial is that it gives a “solid” conceptual framework for OOP to isolate concerns in the code. As stated in my article on reducing coupling, mixing concerns across code will cause significant technical debt and tightly coupled logic that will lead to high-risk regressions. SOLID builds on the OOP paradigm to make it easier to isolate concerns and keep code more loosely coupled.

SOLID is a practice that keeps loosely coupled and building out a strong maintainable codebase for a project. The more loosely coupled your code is, the easier it will be able to change and mold over time. Also, it promotes fewer risks to regressive bugs or expensive refactors from new business demands. With reaching that understanding, we can then ask ourselves: how do we apply these concepts to non-OOP language paradigms?

Applications of SOLID to Functional Programming

SOLID has some great concepts to make the rise of OOP more sustainable. As OOP took off in popular languages such as C++, Java, and C#, SOLID became popular as a guideline for how to keep concerns separated. Recently FP has grown more popular.

Popular languages such as Python, Javascript, and Rust support OOP but inherently are often used more with FP. A lot of languages today can be used for OOP or FP to some degree and even interchangeably. However, let’s explore how SOLID can help FP as we already saw how it traditionally helps with OOP.

Breaking down SOLID for Functional Programming

  • S (Single-Responsibility Principle; SRP): The focus of each function or closure is on doing one thing and one thing only. If the code execution has multiple concerns in it, break them out to new functions that do just one thing. Keep breaking things down until everything has been broken down to the smallest possible units (within reason).
  • O (Open-closed Principle; OCP): FP achieves similar results by using high order functions. A pure function does one thing that a high order function can extend. Then, enhance/modify the output or input without touching the original function.
  • L (Liskov Substitution Principle; LSP): Interfaces are essential for maintainable code even in FP, but sadly not all FP languages support interfaces. Hopefully, over time more FP languages adopt interfaces. Interfaces are crucial for LSP and allow compile-time checks in the build process. Without compile-time checks, one will need more documentation or automated testing to define the interface’s responsibility manually.
  • I (Interface Segregation Principle; ISP): Interfaces help with ISP for FP if they are available. If not, destructuring assignments (or parallel assignments) are capable of clearly defining the definition of inputs and outputs for a given function or closure.
  • D (Dependency Inversion Principle; DIP): For FP, this is similar to ISP, where interfaces break the need to rely on concrete implementations being passed into functions. If these are not available, destructuring or documentation can help with defining the needed inputs and outputs. Promote pure functions by passing in the state they require. They should operate without side effects and return a consistent output.

Take away guidelines to follow

  • Always use interfaces whenever possible and don’t tie to concrete implementations. Not all languages have interfaces, but use them when they are available to ensure concrete implementations are not directly coupled. Interfaces create a more loose coupling and should represent a single concern.
  • Favor documentation for interfaces and not implementations. Since implementations are exposed through an interface, always document the interface since its purpose is to expose specific functionality of the concern. Implementations need much less documentation in favor of interface documentation.
  • Interfaces (especially) and implementations are specific to one concern. Specificity makes things more readable, easy for other developers to follow and maintain over time. Move other concerns out to other modules and pass the state into the interface or implementation as needed (composition).
  • Base classes used for abstractions should be clean of all conditionals and complex logic. If you are adding an “if” statement to a base class, you should question the design. Further, if you are adding functionality to a base class that is not used by all children, double-think the location of the concern. An abstract base class should be as simple as possible. Move details to the children of the abstraction.