If you are interested in reading this article in Spanish 🇪🇸, check out my blog:
The Developer's Dungeon
Yesterday I was browsing one of the online developer communities where I usually hang out and I noticed there was this super intense debate about what writing good code is all about. As usual, the terms Design Patterns, Clean Code, and SOLID where being thrown around a vicious fight of programming terms. It was clear that the audience was mainly Object-Oriented programmers. Suddenly, someone started talking about Functional Programming, me as an OOP programmer who is in his path to absorb as much as I can from the Functional way, I got intrigued.
This person argued that SOLID and Design Patterns are Object-Oriented Programming(OOP) stuff and they don't relate well with Functional Programming(FP).
This argument got stuck inside my head so I decided to write about it to express my own personal opinion. Since I have two different opinions on SOLID and Design Patterns, today, I am gonna cover only SOLID, I may cover Desing Patterns in the near future though.
Let's start with some introductions.
SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable
They were first introduced by Robert C. Martin in the early 2000s in his paper Design Principles and
Design Patterns and they are the following:
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Let's go one by one and let's see how they apply to both paradigms.
"An object should only have a single responsibility, that is, only changes to one part of the software's specification should be able to affect the specification of the object."
Traditionally when people talk about this principle they think about classes (although the original idea comes from UNIX development), they think about extracting behavior into multiple classes and handling a proper separation of concerns.
Although functional programming languages don't have classes the same principle holds true. Functions should be small reusable pieces of code that you can compose freely to create complex behavior.
This can be extracted to almost anything, once your functions are small, the modules where they are located they should also form a cohesive closure that does only one thing and does it well.
As long as your function or class or module has only one reason to change then you are applying this principle.
"Software entities ... should be open for extension, but closed for modification."
This principle is usually instantly related to inheritance. A well-defined parent class that holds functionality and children of this class extend or reuse the mentioned functionality. In reality, it just means that we should be able to reuse and extend code without having to modify the original implementation.
Instead of using inheritance, Functional Programming achieves this by using two tools. Composition to create new behaviors from previously defined functions and higher-order functions to change functionality at runtime, btw if you are interested in reading more about these topics you can check my series Functional Programming for the object-oriented developer.
"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."
Again when people generally think about this principle the first idea that comes to their head is that if the parent class has some behavior, their children should not break that behavior, but this is not the only applicable case, LSP also applies in case we use generic or parametric programming where we create functions that work on a variety of types, they all hold a common truth that makes them interchangeable.
This pattern is super common in functional programming, where you create functions that embrace polymorphic types (aka generics) to ensure that one set of inputs can seamlessly be substituted for another without any changes to the underlying code.
"Many client-specific interfaces are better than one general-purpose interface."
This is an easy one, but many people, including the ones that brought the topic to my attention, get too attached to the word "interface" and they automatically refer to the concept of interfaces in languages like C# or Java, they think that if you don't have interfaces then this principle cannot be applied.
In reality, every interaction between components is done by an interface. When you use functions from a module, you are using the disposed interface of that module, even if we are in a dynamically typed language, that interface still exists. The point of this is that the way you create modules(or classes or interfaces or API's or whatever) needs to be cohesive, you should provide one clear way of doing things instead of many, and you should expose only what is necessary for the users to perform the specific task.
"One should depend upon abstractions, [not] concretions."
In languages like C#, this is achieved by using two tools. One is to create interfaces to define contracts of a predefined functionality. The other is to use dependency injection so that users of that functionality don't manually instantiate the concrete class, instead, they receive an instance of the interface through their constructor and they just call the appropriate methods on the instance.
In functional programming, abstractions are the default way of handling code, functions are abstractions too, especially in functional programming where we care more about the "shape" of the data instead of to which specific type they are attached to. This creates the possibility to freely change the implementation at runtime by passing functions as parameters to other functions or even returning functions as results from the computation.
I hope you liked my take on SOLID and Functional Programming. If you would like to see a direct comparison between implementations of Design Patterns in OOP and FP please let me know below in the comments 😄
As always, if you liked this post please share it on social media.