Software design using OOP + FP — Part 1
Published: 2024-06-27 02:01:16


## Software design using OOP + FP — Part 1

Let’s do crazy solutions combining OOP and FP :)

![](https://cdn-images-1.medium.com/max/5478/1*xFZOawpc9h81Voxftmc1Pg.jpeg)

The idea for this article is to summarize several patterns and provide examples of how to solve certain problem cases by mixing Object-Oriented Programming (**OOP**) and Functional Programming (**FP**) to get better solutions.

Throughout our professional lives, we can move between different programming paradigms. Each has its strengths and weaknesses and there are more than just two. We will explore OOP and FP because they are the most commonly used. The key is always knowing when and how to apply each to get better solutions.

Let’s get a quick review of the main ideas/concepts of each paradigm

### Object-oriented programming features

* *Encapsulation*: Hiding internal details and exposing only what’s necessary.

* *Inheritance*: Creating new classes based on existing ones.

* *Polymorphism*: Different classes can be treated through a common interface.

* *Abstraction*: Simplifying complex systems by hiding unnecessary details.

* *Dynamic Binding*: Determining which method to invoke at runtime rather than compile time.

* *Message Passing*: Objects communicate with one another by sending and receiving messages.

### Functional programming features

* *Immutability*: Data doesn’t change once created.

* *Pure functions*: Always return the same result for the same inputs.

* *Function composition*: Combining simple functions to create more complex ones.

* *Recursion*: Solving problems by breaking them down into smaller cases of the same problem.

* *High order*: Functions that take other functions as arguments or return functions as results.

* *Referential transparency*: An expression can be replaced with its value without changing the program’s behaviour.

Now that we have reviewed the principal paradigms topics, we are ready to check some common smell patterns but before that, I want to present my top premises when I am coding

* Minimize ( as much as possible ) the amount of used syntax.

* Avoid unnecessary parameter definitions.

* Keep functions/methods small and focused on single-purpose ( inline if possible ).

* Design using immutability and leave mutability just for special cases ( encapsulated ).

* Use descriptive names for functions, methods, classes and constants.

* Avoid using built-in iteration keywords ( unless using a high-order function makes the solution worse ).

* Keep in mind the computational cost all the time.

* The code must explain itself.

**Note**: I will use *NodeJs *for these examples because of facilities although these ideas should be easily implemented using any other programming language that allows us to use OOP and FP paradigms.

**Complex if then else and switch cases**

Several times I see code with many nested conditions or switch cases that make the logic hard to understand and maintain

    const reactByAction = action => {
      switch (action.type) {
        case 'A':
          return calculateA(action)
        case 'B':
          return calculateB(action)
        case 'C':
          return calculateC(action)
        default:
          return // Do nothing
      }
    }

Here we can choose between two approaches. We can decide to use the classic *Polymorphism *on OOP, delegating the responsibility of that computation to every action representation

    const reactByAction = action => action.calculate()

Where each action knows how to be calculated

`    abstract class Action {
        abstract calculate()
    }
    
    class ActionA extends Action {
        calculate() {
            ...
        }
    }
    
    class ActionB extends Action {
        calculate() {
            ...
        }
    }
    
    ...`

But, indeed, sometimes our solutions weren’t thought to be treated like instance objects. In this case, we can use classical object mapping and *Referential transparency*, which works like an indirect switch case, but allows us to omit complex matches

`    const calculateAction = {
        A: action => ...,
        B: action => ...,
        C: action => ...,
    }`
    
    const reactByAction = action => calculateAction?.[action.type](action)

Of course, this has a limitation, each time you need to “identify” an action type, you will need to create a new constant object mapper.

Whatever the solution is chosen, you always want to keep it simple, delegating the responsibilities the best way possible, distributing the count of lines of your solution and using a good declarative name to simplify the understanding/scalability.

If you can’t provide an instance of every action, I suggest to implement a melted implementation

`    class Handler {
      constructor() {
        this.actionMap = {
          A: action => new ActionA(action),
          B: action => new ActionB(action),
          C: action => new ActionC(action),
        };
      }
    
      reactByAction = action => this.actionMap[action.type]?.(action).calculate()
    }`

**Unnecessary parameter definitions**

Sometimes, we implement functions/methods with many parameters that could be derived from a shared context, here is a simple example
`
    const totalPrice = (price, taxRate, discountRate) => {
        const taxAmount = price * taxRate
        const discountAmount = price * discountRate
        return price + taxAmount - discountAmount
    }
    
    const totalForRegularCustomer = price => totalPrice(price, 0.1, 0)
    const totalForPremiumCustomer = price => totalPrice(price, 0.1, 0.1)`

The definitions are right but we can simplify the number of responsibilities by decoupling the function, using the concept of currying and classes to encapsulate the details and create more specialized methods

Here is a simple suggestion using OOP

`    class Customer {
      taxAmount = price => price * this.taxRate()
      discountAmount = price => price * this.discountRate()
      totalPrice = price => price + this.taxAmount(price) - this.discountAmount(price)
    }
    
    class RegularCustomer extends Customer {
      taxRate = () => 0.1
      discountRate = () => 0
    }
    
    class PremiumCustomer extends Customer {
      taxRate = () => 0.05
      discountRate = () => 0.1
    }`

Note that I used the pattern template method :)

And here is another option using FP

`    const tax = taxRate => price => price * taxRate
    const discount = discountRate => price => price * discountRate
    
    const regularTaxAmout = tax(0.1)
    const premiumTaxAmout = tax(0.05)
    const regularDiscount = discount(0)
    const premiumDiscount = discount(0.1)
    
    const calculationWithRates = (taxRateFunc, discountRateFunc) => price =>
      price + taxRateFunc(price) - discountRateFunc(price)
    
    const totalForRegularCustomer = calculationWithRates(regularTaxAmout, regularDiscount)
    const totalForPremiumCustomer = calculationWithRates(premiumTaxAmout, premiumDiscount)`

Note that I am currying the functions to provide more abstractions increasing the declarativity

Again, we can mix both paradigms in a synthesized design :)

`    class Customer {
      constructor(taxRate, discountRate) {
        this.taxRate = taxRate
        this.discountRate = discountRate
      }
    
      totalPrice = price => price + this.calculateTax(price) - this.calculateDiscount(price)
    
      calculateTax = price => price * this.taxRate
      calculateDiscount = price => price * this.discountRate
    }
    
    const regularCustomer = new Customer(0.1, 0)
    const premiumCustomer = new Customer(0.05, 0.1)
    
    const total = customer => customer.totalPrice
    
    const totalForRegularCustomer = total(regularCustomer)
    const totalForPremiumCustomer = total(premiumCustomer)`

**Excessive long method/functions**

Another classic I’m used to seeing are methods and functions with a lot of responsibilities, too long, with hardcoded conventions and complex business rules

`    const tokenize = code => {
        let tokens = []
        let currentToken = ''
        let currentType = null
        
        for (let i = 0; i < code.length; i++) {
            let char = code[i]
            
            if (char === ' ' || char === '
' || char === '	') {
                if (currentToken !== '') {
                    tokens.push({ type: currentType, value: currentToken })
                    currentToken = ''
                    currentType = null
                }
            } else if (char === '+' || char === '-' || char === '*' || char === '/') {
                if (currentToken !== '') {
                    tokens.push({ type: currentType, value: currentToken })
                    currentToken = ''
                }
                tokens.push({ type: 'operator', value: char });
            } else if (char >= '0' && char <= '9') {
                if (currentType !== 'number' && currentToken !== '') {
                    tokens.push({ type: currentType, value: currentToken })
                    currentToken = ''
                }
                currentType = 'number'
                currentToken += char
            } else if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')) {
                if (currentType !== 'identifier' && currentToken !== '') {
                    tokens.push({ type: currentType, value: currentToken })
                    currentToken = ''
                }
                currentType = 'identifier'
                currentToken += char
            }
        }
        
        if (currentToken !== '') {
            tokens.push({ type: currentType, value: currentToken })
        }
        
        return tokens
    }`

Too complex, right? It is impossible to understand just by reading the code

Let’s see a reimplementation of this using OOP creating an abstraction and encapsulating the mutability of the context

`    const binaryOperators = ['+', '-', '*', '/']
    
    const SPECIAL_CHARS = {
        EMPTY_STRING: '',
        ...
    }
    
    class Context  {
        constructor(code) {
            this.code = code
            this.tokens = []
            this.currentToken = ''
            this.currentType = null
        }
    
        ...
    }
    
    class Tokenizer {
        constructor(code) { this.context = new Context(code) }
    
        getCurrentToken() { this.context.getCurrentToken() }
    
        tokenize() {
            this.context.getCode().forEach(this.processChar)
            this.finalizeToken()
            return this.context.getTokens()
        }
    
        processChar(char) {
            if (this.isWhitespace(char)) {
                this.finalizeToken()
            } else {
                ...
            }
        }
    
        isEmptyChar() {
            return this.getCurrentToken() == SPECIAL_CHARS.EMPTY_STRING
        }
    
        finalizeToken() {
            if (!isEmptyChar()) {
                this.addToken(this.context.currentType(), this.currentToken())
                this.resetCurrentToken()
            }
        }
    
        addToken(type, value) {
            this.context.addToken({ type, value })
        }
    
        resetCurrentToken() {
            this.context.resetCurrentToken()
        }
    
        processDigit(char) {
            if (!this.context.currentIsNumber()) {
                this.finalizeToken();
                this.context.setNumber()
            }
    
            this.context.nextChar()
        }
    
        processLetter(char) {
            ...
        }
    
        isWhitespace = char => char === SPECIAL_CHARS.SPACE || char === SPECIAL_CHARS.JUMP || char === SPECIAL_CHARS.TAB
    
        isOperator = binaryOperators.includes
    
        ...
    }`

Or we can think of a solution using some other FP concepts

`    const isWhitespace = [' ', '
', '	'].includes
    const isOperator = ['+', '-', '*', '/'].includes
    ...
    
    const createToken = (type, value) => ({ type, value })
    
    const initialState = () => ({ tokens: [], currentToken: EMPTY_STRING, currentType: null })
    
    const processChar = (state, char) => {
        if (isWhitespace(char)) return finalizeToken(state)
        if (isOperator(char)) return processOperator(state, char)
        if (isDigit(char)) return processDigit(state, char)
        if (isLetter(char)) return processLetter(state, char)
    
        return state
    }
    
    const finalizeToken = state => isEmptyString(currentToken) ? state : ({
        ...state,
        tokens: [ ...state.tokens, createToken(state.currentType, state.currentToken) ],
        currentToken: '',
        currentType: null
    })
     
    const processOperator = (state, char) => ({
        ...finalizeToken(state),
        tokens: [...state.tokens, createToken(TYPES.op, char)]
    })
    
    const processDigit = (state, char) => ({
        ...state,
        currentType: TYPES.Number,
        currentToken: isNumber(state.currentType) ? state.currentToken + char : char
    })
    
    ...
    
    const tokenize = code => finalizeToken(code.reduce(processChar, initialState())).tokens

Again, we think in encapsulation, abstractions and declarativeness

And, of course, we can provide a mixed solution ;)

    const SPECIAL_CHARS = {
      SPACE: ' ',
      NEWLINE: '
',
      TAB: '	'
    }
    
    const binaryOperation = ['+', '-', '*', '/']
    
    class Tokenizer {
      constructor(code) {
        this.code = code
        this.tokens = []
        this.currentToken = EMPTY_STRING
        this.currentType = null
      }
    
      tokenize() {
        this.code.split('').forEach(this.processChar.bind(this))
        this.finalizeToken()
        return this.tokens
      }
    
      processChar = char => {
        const processors = [
          { predicate: this.isWhitespace, action: this.handleWhitespace },
          { predicate: this.isOperator, action: this.handleOperator },
          { predicate: this.isDigit, action: this.handleDigit },
          { predicate: this.isLetter, action: this.handleLetter }
        ]
    
        const { action } = processors.find(({ predicate }) => predicate(char)) || EMPTY_OBJECT
        action?.(this, char)
      }
    
      isWhitespace = Object.values(SPECIAL_CHARS).includes
      isOperator = binaryOperation.includes
      isDigit = /[0-9]/.test
      isLetter = /[a-zA-Z]/.test
      handleWhitespace = this.finalizeToken
      
      handleOperator = char => {
        this.finalizeToken()
        this.addToken(TYPES.operation, char)
      }
    
      handleDigit = char => {
        if (!this.isNumber()) { this.finalizeToken(); this.currentType = TYPES.number }
        this.currentToken += char
      }
    
      handleLetter = char => {
        if (!this.isIdentifier()) { this.finalizeToken(); this.currentType = TYPES.id }
        this.currentToken += char
      }
    
      finalizeToken() {
        if (!this.isEmpty()) {
          this.addToken(this.currentType, this.currentToken)
          this.currentToken = SPECIAL_CHARS.empty
          this.currentType = null
        }
      }
    
      addToken(type, value) {
        this.tokens.push({ type, value })
      }
    }
    
    const tokenize = code => new Tokenizer(code).tokenize()
`
Let's finish this first article about OOP + FP software design, I would like to write more about this perspective reviewing much more complex examples and patterns, proposing melted solutions and exploring deeply the ideas of both paradigms.

Let me know if something is strange, or if you find something in this article that could be improved. Like our software, this is an incremental iterative process, improving over time 😃.

Thanks a lot for reading!