Functional Object Composition and MVC

Gluing together encapsulation and functional programming

I just read a great blog post on the internet about MVC not being object-oriented, and I could not agree more — after all, we don’t peel an apple the same way do with a banana. However, heads up: I am not an OOP purist, and I believe procedural programming has its place among apps written in that paradigm.

The author of the referred to post suggests object composition as an alternative to replacing MVC layers. In his words, MVC takes the opposite direction of OOP: it exposes the data and hides behavior, while the latter hides the data and exposes behavior. The controller is in charge and fetches naked data from the model, makes assumptions about it, and injects the result into the view. A single object does not retain knowledge about the data, which means there is no encapsulation, and therefore no OOP. ☑

It gets even worse when doing MVC by the book. Classic MVC apps are comprised of only controllers, models, and views. Hence each and every responsibility gets distributed somehow among these layers. This convention is not necessarily wrong: it empowers beginners to get off the ground quickly and allows them to figure out in a snap where logic goes. It is good for prototyping and bad for mostly everything else — unless we make a conscious effort to spreading out the M layer across smaller objects.

Now I ask: is it possible to use MVC while still leveraging OOP benefits? The answer is yes. To be honest, I don’t care if MVC is OOP or not, because it can as well behave as the delivery mechanism for such apps; I mostly care if the code is reusable when it needs to be.

As long as we reach the same maintainability benefits that we aim for by means of these abstractions, there is no reason to go nuts! In other words, I’d certainly worry about dodging away from MVC in a framework that adopts it as the default delivery mechanism, though I would not give up on OOP!

This partially theoretical post explores the following topics:

  • Using OOP in an MVC app while gleaning encapsulation benefits.

  • Going further and using object composition instead of MVC. How would it be? Where to use it?

  • Imagining object composition as a first class feature of OO languages.

Making it more OOP

Let’s start with an MVC example written in Rails which we’re all familiar with:

# Controller
class BooksController < ApplicationController
  def index
    @books = Book.all.includes(:author)
  end
end

# Model
class Book < ApplicationRecord
  belongs_to :author
end
<!-- View -->
<article>
  <% @books.each do |book| %>
    <h2><%= book.name %> by <%= book.author.name %></h2>

    <%= paragrapharize book.description %>
  <% end %>
</article>

The following points are worth making clear:

  • We can’t easily reuse the controller.

  • The @books collection holds Book instances which are used in the view layer. We access their attributes directly there, and sometimes (sic) even in the controller.

  • We can still reuse the model independently.

  • We can reuse the view regardless.

Clearly, our biggest concern pertains to the controller: it is highly coupled to the router and sometimes to the views, especially in a framework like Rails. However, we can move its logic to another class:

class BooksList
  def call
    Book.all.includes(:author)
  end
end

And call it like this:

class BooksController
  def index
    @books = BooksList.new.()
  end
end

So far so good. Now let’s suppose we need an endpoint to filter books by gender and other attributes – it’s essentially the same logic with new functionality added on top. Our requirements demand this to be a distinct resource:

# Controller
class BooksFilterController < ApplicationController
  def index
    filtered_books = BooksFilter.new(BooksList.new, params).()
    @books = filtered_books.map { |book| BookView.new(book) }

    render 'books/index'
  end
end

# Model
class Book < ApplicationRecord
  belongs_to :author

  def author_name
    author.name
  end
end

# Representation of a book to be used in the template.
class BookView
  delegate :title, :author_name, to: :@book

  def initialize(book)
    @book = book
  end

  def description
    Sentence.new(@book.description)
  end
end
<!-- View -->
<article>
  <% @books.each do |book| %>
    <h2><%= book.title %> by <%= book.author_name %></h2>

    <%= book.description %>
  <% end %>
</article>

Although our new controller shares most of its purpose with BooksController (while still being different), we still had to write it! Nonetheless, this is attenuated by the fact that our code became DRY due to both controllers sharing common logic. Also, we went a great way toward making it more object-oriented:

  • BooksList retains knowledge of how to fetch a books list.

  • BooksFilter knows how to filter books.

  • We are composing these two objects together.

  • We added a BookView object to clean up the template, and it uses object-oriented helpers: imagine Sentence implementing to_s and to_str methods.

  • We removed procedural logic from the view.

  • Neither the controller nor the view knows about the innards of a book data structure. This is taken care of by BookView.

Now, that’s object-oriented programming!

Still, the controller and the view are not object-oriented, but I’m OK with this: the view is reusable and it counts with a presenter layer, and we moved our controller’s logic to another object.

Refactoring to object composition

The controller is a piece of boilerplate that we need to write every time, and it’s the default mechanism by which we hook into the views. Can we improve our situation if we make the VC part of our code more object-oriented?

Let’s envision what we have so far closer to what the article suggests, starting off from the router:

get '/all_books' do
  PrintedBooks.new(BooksList.new).()
end

get '/books' do |params|
  PrintedBooks.new(
    BooksFilter.new(
      BooksList.new,
      params
    )
  ).()
end

I like this version! Controllers are gone and have been transformed into flexible objects. If you are wondering about PrintedBooks, let’s imagine it being something like this:

class PrintedBooks
  def initialize(books)
    @books = books.map { |book| BookView.new(book) }
  end

  def call
    Renderer.new('books/index', books: @books).()
  end
end

PrintedBooks is playing the role of the controller, except that it’s now composable and it knows what to do with its data, so we just tell it: printed_books.call.

What’s unusual about this approach is that we see at a glance everything the action does— all steps are utterly explicit, and they communicate us at a high level how the code works. Also, we are calling our renderer inside PrintedBooks, and that may be a good thing: we finally know where the rendering functionality comes from!

Furthermore, it’s obvious where to put new functionality, and we are not worrying about categorizing objects as controllers, models, views, presenters, services, etc.— which can be liberating! Some clever namespacing would likely lend more shape to our code and make it a breeze to maintain while revealing the domain more clearly (`Library::PrintedBooks is cool, don’t you think?).

Finally, one aspect sprung out after shifting our mindset to object composition: we are working with an object pipeline! And that reminds me of functional programming.

Object composition as a first class citizen

What if Ruby had a pipe operator akin to Elixir’s to compose objects? It would preserve its data encapsulation characteristics and still incorporate functional programming concepts such as first-class functions.

That feature would be even better alongside some sort of enforced object immutability, for example, “only assign instance variables at the constructor.”

Let’s picture an imaginary version of Ruby:

get '/all_books' do
  (&BooksList.new |> &PrintedBooks.new).call
end

get '/books' do |params|
  view = &BooksList.new
  |> &BooksFilter.new(params)
  |> &PrintedBooks.new

  view.call
end

Although equivalent, it reads much better than our pure-Ruby example. If you’ve heard about Elixir’s pipe operator, I bet this code makes you feel at home!

While we should go from the inside-out in our former example to understand the flow, here we take the opposite direction and pass the result of the current expression as the first parameter of the expression that follows until getting the final object. This page explains the idea in more detail.

I’m sure such a feature would encourage us to compose objects and functions more frequently, and it would change the way we think about object-oriented programming.

OK, where can I use this?

I’m not aware of any language that implements “first class object composition”, but you can apply our MVC-equivalent code today in Ruby frameworks like Sinatra and Roda. With these tools, you are free to roll your own app however you want (if you know what you’re doing!). For instance, this could very well be a Sinatra application:

get '/all_books' do
  PrintedBooks.new(BooksList.new).()
end

get '/books' do |params|
  PrintedBooks.new(
    BooksFilter.new(
      BooksList.new,
      params
    )
  ).()
end

As for Rails applications, you can still enjoy object composition in non-MVC parts — but you have to obey some conventions if you want to succeed.

Finally, it’s important to say: don’t allow convoluted logic to spread inside router blocks! This attitude forces you to keep things clean and figure out exactly which objects you need in your pipeline and how they fit together.

Conclusion

I dig object composition, and we can extend this line of thinking beyond MVC. If you’ve worked with functional programming before, you certainly know how flexible function composition can be, and the same may apply to objects which retain control of their data.

I don’t think there exists a winner between OOP and FP; they are different beasts. Each paradigm offers a way of thinking and reasoning about programs, regardless of flaws they may have developed over time. But we can certainly have FP concepts applied to OOP for the win. And why not, apply OOP concepts to FP for the win!

We want to work with you. Check out our "What We Do" section!