DEV Community

Ustun Ozgur
Ustun Ozgur

Posted on

Object Oriented Functional Programming or How Can You Use Classes as Redux Reducers

Note: This article originally appeared Ustun Ozgur's blog on Medium.

TL;DR You can use ImmutableJS Record classes with methods as Redux reducers, combining the best of FP and OOP.
See the final result here: https://gist.github.com/ustun/f55dc03ff3f7a0c169c517b459d59810

In the last decade, functional programming has been steadily gaining
popularity, while object oriented programming is being questioned more
and more. The kingdom of nouns is now being threatened by the kingdom
of verbs, and we can see this revolution best explained in Rich
Hickey's talk Simple Made Easy.

In the JavaScript frontend ecosystem, React broke the last functional
frontier, UI development and ideas from the functional world such as
immutability, higher order functions are now becoming common-place in
the industry.

The principal difference between object oriented programs and
functional programs is their stance on handling data and
state. Objects by their nature encapsulate data, whereas in functional
programs, data is usually separated from the code. One additional
vital difference is that, most OOP systems also incorporate identity
tracking, that is, an object is not only the sum of its state (data)
and methods (or functions in FP world), but also identity.

So,

  • OOP out of the box gives you identity + state + methods.
  • FP out of the box gives you data + functions.

Tracking the identity is left as exercise to the reader, which is a
blessing and a curse; and as a consultant and trainer to multiple
companies, the single most source of confusion people face when
transitioning paradigms.

Decoupling

The fundamental idea in analyzing big systems is decoupling and layering. When confronted with state, functional programming basically asks the
following question: What if we would take the three notions,
state, identity and methods and decouple them?

The advantage is that these different parts can be constructed and
assembled separately. The disadvantage is you risk losing the cohesion
of your abstractions.

  1. Functions and Methods

Let's start with methods for example. Most classes act as bags of
methods, so if you have a few methods on your plate, you could
actually have those as different functions that take the primary data
being operated on as the first argument. Effectively, thing.doIt() becomes doIt(thing).

Such functions can obviously take additional arguments, however most
of the time, in a business application setting which follows the
Domain Model pattern, the first argument of the function will be the
domain model we are operating on.

As the number of functions increases though, your program is in a
danger of filling up with lots of functions scattered around. FP
languages do not give much guidance here, effectively you are free to
do whatever you prefer. Again a blessing and a curse.

In an OOP world, where a function goes in is pretty much defined; in
less flexible languages like Java (before Java 8) for example, the
functions belonged to classes.

In a more flexible language like JavaScript though, we could collect
the functions related to a data structure in a module or an object
literal.

For example, if we have 3 different functions operating on a data
structure like Person, we could collect three functions operating on
Person datas as follows:

PersonFunctions = {
 doThis(person, …) { … }
 doThat(person, …) { … }
 doBar(person, …) { … }
}

This is effectively solving the third part of the decoupling process,
namely handling the placement of the methods.

Another alternative here would be to create a JS module (a file
actually) that has these functions at the top-level, as follows:
in person_functions.js
function doThis(person, …) { ….}
function doThat(person, …) { ….}
function doBar(person, …) { ….}

(In a langugage like Clojure, for example, the equivalent would be to put these functions into namespaces.)

  1. State, Data and Identity

As mentioned before, functional programs effectively separate state
(data) and identity. Most OOP systems operate the data in place,
whereas the functional counterparts need to handle both the input and
output of the data explicitly. Hence, in OOP, this keyword offers a convenience to the following 3 steps in a functional program:

a – get data => state as data
b – transform data => some_function(data)
c – put the data where you took it. => state = some_function(data)

In OOP world, steps a & c are automatic, if you access the state in
the thing pointed by this keyword. This is the main decoupling here, OOP takes the position that most of the time, you will put the data from where you took it back, where FP takes the position that these three steps could be decoupled.

If you want to track the identity in an FP system, you have to do it
manually, though it is not as laborous as it sounds.

For example, Clojure provides atoms, which effectively are more similar to objects in Java or JavaScript; which enclose the pure data.

Any function call operating on an atom effectively sends the same call to the inner object, and writes the output object back.

Let's say we have an atom that wraps some data.

my_object = atom(data)
swap(my_object, some_function)

effectively becomes three operations:

1- Extract the data from the object.
2- Execute some function on the data.
3- Write the data back into the object.

As a result, if identity tracking is added, a FP system is
equivalent to an OOP system.

Redux

And this is where Redux comes in. Redux is basically advertised as “a
state container”, which wraps your data (state) in an object
(store). And any transformation you do is a transforming function
called “a reducer”.

Excluding the fancy terms like state containment and reducing
operation though, this is effectively just what OOP provides. OOP
provides a container for your data, and provides some methods
(equivalent to functions, reducers) that operate on that data, and put
the result back into the place when the transformation is done.
Hence, Redux reducers are equivalent to traditional Object Oriented
Programming, with the following two differences:

1- It does not give you dispatch by default, so you have to do if/else/switch to select the method to operate on.
2- All the data is modelled as immutable data structures.

So, the obvious question is this: Can we have our cake and eat it too?

That is, how can someone proficient with object modeling reuse his
skills in a Redux application?

The Obligatory Todo App

Let's consider the following transform function for a TodoApp, a reducer. The basic domain modeling is as follows:

  • You can add, remove todos, toggle todos' completion state, and add a  temporary todo text that will be added when user presses  Submit. I'll just implement REMOVE_TODOS so that the code is  concise.
function todoAppReducer(state={todos:[], newTodo: ‘'}, action) {
    switch (action.type) {
    case ‘REMOVE_TODO':
            return {…state, todos: state.todos.filter(todo=>todo.description!= action.payload.description)}
    case ‘ADD_TODO':
    case ‘TOGGLE_TODO':
    case ‘ADD_TEMP_TODO':
    }
}
Enter fullscreen mode Exit fullscreen mode

The first refactoring results in the following, where we replace dispatch functions with an object bag of methods.

function todoAppReducer(state={todos:[], newTodo: ‘'}, action) {
    methods = {
    REMOVE_TODO: function (payload) return {…state, todos: state.todos.filter(todo=>todo.description != payload.description)},
    ADD_TODO: function () …,
    TOGGLE_TODO: function () …,
    ADD_TEMP_TODO: function ()
    }

    return methods[action.type](action.payload)
}
Enter fullscreen mode Exit fullscreen mode

Now, since the functions in methods object are inside the main function, all of them can access the variable named state. If we take the methods object out those, we have to pass the state explicitly.

methods = {
    REMOVE_TODO: function (state, payload) return {…state, todos: state.todos.filter(todo=>todo.description != payload.description)},
    ADD_TODO: function (state, payload) …,
    TOGGLE_TODO: function (state, payload) …,
    ADD_TEMP_TODO: function (state, payload)
}

function todoAppReducer(state={todos:[], newTodo: ‘'}, action) {
    return methods[action.type](state, action.payload)
}
Enter fullscreen mode Exit fullscreen mode

Now, the object literal methods is starting to look more like a
traditional bag of objects, a class. First, let's move them inside a
proper class, where we do not make use of this for now. Effectively,
this is a class of static methods that take ‘state' as first variable.

class Todo {
     REMOVE_TODO(state, payload) {
     return {…state, todos: state.todos.filter(todo=>todo.description != payload.description)};
    }
    ADD_TODO(state, payload) {
    }
}

Enter fullscreen mode Exit fullscreen mode

At this stage, we are almost midway between FP and OOP. Closer to FP in spirit, and closer to OOP in look. The generation of immutable values is quite ugly though, using spread operator and various tricks that will irk most newcomers.
Enter ImmutableJS library, which makes these transformations natural. Getting a new version of an immutable object with all the fields, except one intact is as simple as just setting that field.
For example, let's say we have object A, and want to get object B, but with name set to foo.

B = A.set(‘name', ‘foo')

Effectively, as an OOP programmer, you can think of ImmutableJS as taking a clone of your current object without defining cloning operation and setting the different values.
Want to have the same as in object A, but with name ‘foo', and surname ‘bar'?
You could do it by setting those in succession:

A.set(‘name', ‘foo').set(‘surname', ‘bar')

or in one step by merging the second object like:

A.merge({name: ‘foo', surname: ‘bar'})

So, transforming our previous class to use ImmutableJs, we get the following:

class Todo {

    REMOVE_TODO(state, payload) {
    return state.set(‘todos', state.todos.filter(todo=>todo.get(‘description') != payload.description));
    }

    ADD_TODO(state, payload) {
    }
}

function todoAppReducer(state=Immutable.fromJS({todos:[], newTodo: ‘'}), action) {
    return Todo[action.type](state, action.payload)
}
Enter fullscreen mode Exit fullscreen mode

You will see that we are still passing state explicitly, whereas we would just use this to pass state explicitly in an OOP application.
Enter Immutable Records, which give you the best of both worlds, where you can define methods that operate on this.
Let's convert our Todo class to make use of Immutable Records.

class Todo extends Immutable.Record({todos:Immutable.List(), newTodo: ‘'}){
    REMOVE_TODO(payload) {
    return this.set(‘todos', state.todos.filter(todo=>todo.get(‘description')!= payload.description));
    }

    ADD_TODO(payload) {

    }
}

function todoAppReducer(state=new Todo(), action) {
    return state[action.type](action.payload)
}
Enter fullscreen mode Exit fullscreen mode

See where we are going with this? Just a few cosmetic steps left.

1- What to do about methods we don't recognize? In JS, this is easy, we could just access the proper state[action.type] and check whether it is a function or not.

2- Ugly method names: In Redux apps, event names are usually CONSTANT_CASED and we want them camelCames. The transformation is easy thanks to lodash.camelcase.

Now, let's extract the part where we take an Immutable Record class and we produce a compatible Redux reducer.

class Todo extends Immutable.Record({todos:Immutable.List(), newTodo: ''}) {

    removeTodo(payload) {
    return this.set(‘todos', state.todos.filter(todo=>todo.get(‘description')!= payload.description));
    }

    addTodo(payload) {
    }
}
function todoAppReducer(state=new Todo(), action) {
    var fn = state[camelcase(action.type)]
    if (fn) {
    return state[camelcase(action.payload)](action)
    } else {
    // we don't recognize the method, return current state.
    return state;
    }
}
Enter fullscreen mode Exit fullscreen mode

Final Product:
You can get the final version of this pattern here on Github

var camelCase = require('lodash.camelcase');
const {Map, Record, List} = require('immutable');

class Todo extends Record({ description: null, completed: false }) {
    toggle() {
        return this.set('completed', !this.completed);
    }
}

const InitialTodoApp = Record({
    newTodo: '',
    todos: List(),
    activeFilter: ''
});


class TodoApp extends InitialTodoApp {

    init(data) {
        return this.merge(data);
    }

    // action methods: kind of like IBActions

    setTempTextAction({value}) {
        return this.setNewTodo(value);
    }

    removeTodoAction({description}) {
        return this.removeTodo(description);
    }

    addTodoAction() {
        return this.addTodo();
    }

    // other methods

    setNewTodo(newTodo) {
        return this.set('newTodo', newTodo);
    }

    addTodo() {
        return this.addTodoFromDescription(this.newTodo).resetNewTodo();
    }

    resetNewTodo() {
        return this.set('newTodo', '');
    }

    addTodoFromDescription(description) {
        const newTodos = this.todos.push(new Todo({ description: description }));
        return this.setTodos(newTodos);
    }

    removeTodo(description) {
        const newTodos = this.todos.filter(todo => todo.description != description);
        return this.setTodos(newTodos);
    }

    setTodos(todos) {
        return this.set('todos', todos);
    }

    setTodosFromJS(todosJS) {
        const todos = todosJS.map(todoJS => new Todo(todoJS));
        return this.setTodos(todos);
    }

    incompleteTodos() {
        return this.todos.filter(todo => !todo.completed);
    }

    nIncompleteTodos() {
        return this.incompleteTodos().length;
    }

    completeTodos() {
        return this.todos.filter(todo => todo.completed);
    }

    nCompleteTodos() {
        return this.completeTodos().length;
    }

    allTodos() {
        return this.todos;
    }

    toggleTodo({description}) {
        var newTodos = this.todos.map(todo => todo.description != description ? todo : todo.toggle())
        return this.setTodos(newTodos);
    }

    describe() {
        console.log(JSON.stringify(this.toJS(), null, 4));
        console.log("incomplete todos", this.nIncompleteTodos());
    }
}

function reducerFromRecordClass(klass) {
    return function (state = new klass(), action) {
        var fn = state[camelCase(action.type + '_ACTION')];
        if (fn) {
            return state[camelCase(action.type + '_ACTION')](action);
        } else {
            if (state[camelCase(action.type)]) {
                console.warn('You tried to call an action method, but no such action method provided.', action.type)
            }
            return state;
        }

    }
}


const todoAppReducer = reducerFromRecordClass(TodoApp);

export default todoAppReducer;
// main();
Enter fullscreen mode Exit fullscreen mode

Compared to a traditional OOP application, we can observe a few things:

1- All setters have to return a new object.
2- Identity tracking is done by redux.
3- Redux actions are suffixed by “action (this is completely optional, just provided to separated methods that are invoked via redux from normal methods. Redux methods simply delegate to normal class methods.)

Other than that,it is pretty much the best of both functional and object oriented worlds.Unlike most Redux applications which operate on an amorph, unnamed
data structure called “state”, we have a real domain model which eases
our mental data abstraction capabilities. We can also reuse this model
elsewhere easily and even use other OOP techniques like inheritance to
derive new classes.

Unlike most OOP applications, this operates on immutable data as in FP
and hence solves the tight coupling between state and identity.
In this particular instance, identity tracking is left to Redux, but a
simple stateful wrapper like a Clojure atom will bring you the
identity tracking benefits of OOP.

Acknowledgments:

Thanks to Ahmet Akilli from T2 Yazilim for introducing me to JumpState, which basically implements the same idea, but without using Immutable Records. See more discussion here: https://medium.com/@machnicki/why-redux-is-not-so-easy-some-alternatives-24816d5ad22d#.912ks1hij

Conclusion

I hope this article provides guidance to you as you utilize hybrid paradigms in developing your applications. We believe FP and OOP paradigms can co-exist to build powerful products.

If you need assistance, consulting and training, feel free to drop us a line at SkyScraper.Tech (contact@skyscraper.tech) and we'll be pleased to help.
We provide consultancy services, where we lead teams, and also
write code. We also provide skeletons so that our customers' existing teams can continue from a good foundation.

We support a number of platforms, ranging from Django to nodejs to
Clojure apps, depending on the requirements. We also give trainings
mainly on JavaScript (backend and frontend), but also on other
platforms we support.

See http://skyscraper.tech for more info.
Discuss this article on HackerNews: https://news.ycombinator.com/item?id=13578656

Top comments (0)