DEV Community

loading...
Cover image for How to implement Design Patterns in the light of Functional programming

How to implement Design Patterns in the light of Functional programming

jorgetovar621 profile image Jorge Tovar ・3 min read

Why Design patterns?

We as software developers don't want to reinvent the wheel, that's one of the reasons why I love Desing Patterns, But I think that we can go a step further and embrace functional programming to deliver simple solutions.

If you are a Software Developer who loves making great things and implements the best practices in the industry, probably you have heard about Design patterns.

Design patterns provided a shared vocabulary and general solutions to design problems. You apply them to your specific application.

In the current state of software and the Object-Oriented Paradigm ruling the world, I started my journey a few years ago by reading Head First Design Patterns, but after that, I started to learn about Functional Programming and the new capabilities of languages like Java - 1.8 Release- and Kotlin and realized that: with Functional Programming, you can implement Design Patterns in a better and more concise way.

Strategy

For example, you can use the Strategy pattern in the OOP way to implement the four basic mathematical operations.

package com.jetprogramming.designpatterns.headfirst.strategy

interface Strategy {
    fun doOperation(num1: Int, num2: Int): Int
}

class OperationAdd : Strategy {
    override fun doOperation(num1: Int, num2: Int): Int {
        return num1 + num2
    }
}

class OperationMultiply : Strategy {
    override fun doOperation(num1: Int, num2: Int): Int {
        return num1 * num2
    }
}

class Context(private val strategy: Strategy) {

    fun execute(num1: Int, num2: Int): Int {
        return strategy.doOperation(num1, num2)
    }
}
Enter fullscreen mode Exit fullscreen mode

With this design is easy to add new Operations only by extending the current code.

package com.jetprogramming.designpatterns.headfirst.strategy

fun main() {
    val contextAdd = Context(OperationAdd())
    val contextMultiply = Context(OperationMultiply())

    println(contextAdd.execute(1, 2))
    println(contextMultiply.execute(3, 2))
}
Enter fullscreen mode Exit fullscreen mode

But..., I need a new class for every new Operation, can we do it better?. Maybe using Functions. Let's give it a try.

package com.jetprogramming.designpatterns.headfirst.strategy

class ContextFn(private val fn: (a: Int, b: Int) -> Int) {

    fun execute(n1: Int, n2: Int) = fn.invoke(n1, n2)
}
Enter fullscreen mode Exit fullscreen mode

Kotlin is a powerful language that allows you to write code in functional style or OOP, it's up to you to decide the best solution for your context.

In the Functional way, I only need to add a subtraction function to extend the current code, no new classes! no more ceremony.

package com.jetprogramming.designpatterns.headfirst.strategy

fun main() {
    val contextFnSum = ContextFn { x: Int, y: Int -> x + y }
    val contextFnMultiply = ContextFn { x: Int, y: Int -> x * y }
    val contextFnSubtraction = ContextFn { x: Int, y: Int -> x - y }

    println(contextFnSum.execute(1, 2))
    println(contextFnMultiply.execute(3, 2))
    println(contextFnSubtraction.execute(3, 2))
}
Enter fullscreen mode Exit fullscreen mode

Wow, for my this was open eyer, working with pure functions, and composition is a clean way to design solutions! But what about full Functional Programming Languages like Clojure, Can you implement a solution like that without classes and objects?. Of course.

(defn context [f n1 n2]
      (f n1 n2))

(defn -main
      "I don't do a whole lot ... yet."
      [& args]
      (println (context + 1 2))
      (println (context * 1 2))
      (println (context - 1 2))
      )
Enter fullscreen mode Exit fullscreen mode

You know you don't want to reinvent the wheel, I think that by having the shared vocabulary of Design Patterns and making your solutions with the Business Logic in the core, with pure functions and immutable data structures, You can separate in a clean way, what changes from what stays the same, and deliver better outcomes with simple solutions.

Command:

This is another important pattern in OOP when we can encapsulate method invocation so the object invoking the computation doesn’t need to worry about how to do things.

interface Command {
    fun execute()
}

class LoginCommand(private val user: String, private val password: String) : Command {
    override fun execute() {
        DB.login(user, password)
    }
}

class LogoutCommand(private val user: String) : Command {
    override fun execute() {
        DB.logout(user)
    }
}

fun main() {
    val loginCommand = LoginCommand("clojure", "kotlin").execute()
    val logoutCommand = LogoutCommand("clojure").execute()

}
Enter fullscreen mode Exit fullscreen mode

But after all, we know that we can do it better with functions.

(defn execute [command & args]
      (apply command args))

(execute login "clojure" "kotlin")
(execute login "clojure")

Enter fullscreen mode Exit fullscreen mode

In conclusion: the code written with Functions and Design patterns is very modular, composable, reusable, and easy to reason about.

Discussion (0)

pic
Editor guide