Ruby

Decomposing Monolithic View Systems

When we break down a monolithic app into microservices, we go through a process known as decomposition. Simply put: This process can be a bit overwhelming. After all, if you have a really large monolith, you have a ton of code to organize and decompose! Because of this, I believe decomposition is best implemented as a slow and gradual process.

Before we dive into implementation details, we want to figure out what to decompose and what we want to decompose it into.

It’s important to organize each piece of decomposition into areas that are organized around functionality. Some of these groupings can be a bit difficult to figure out. Many apps don’t have well-defined sections. However, a simple place to start is at the view layer.

In a normal MVC-based monolith, the model, view, and controller logic are tightly coupled. With decomposition, we want to extract the view layer into a separate application, while leaving the rest of the application’s existing logic in its place.

To accomplish all of this effectively, we’re going to have to change up the way we pace ourselves writing software.

Breaking Your Development Rhythm

Each team has its own unique cadence by which it makes improvements to software. If we were to guess the process for creating new view layer logic in a monolith, it might look something like this:

  1. Update model and controller logic;
  2. Create view logic;
  3. Profit.

When we start leveraging the separate view service, our process will start to look something like this:

  1. Update model and controller logic – MVC backend;
  2. Serialize resources to be passed into View Service – MVC backend;
  3. Update model and resources to receive backend data – View service;
  4. Create view logic – View service;
  5. Profit.

As you can see, we added a few more steps to our process. It might seem like more of a burden, but the resulting product should help you better distribute the workload throughout your application. We lose the simplicity of keeping the view layer on the server side, but we gain the advantage of delegating certain tasks and functions to the client side!

Let’s take a look at a more technical implementation of this.

hbspt.cta.load(1169977, ‘11903a5d-dfb4-42f2-9dea-9a60171225ca’);

Pulling Apart Our Server-Side Views

The example I want to walk through involves decomposing Rails’ view layer into an EmberJS view service. To do this, I’ll be leveraging a tool called Ember Islands. Ember Islands is a popular Ember add-on that allows us to render Ember components within server-side views.

We achieve this decomposition by leveraging Ember components. Ember components are reusable pieces of Ember views and logic that can be rendered anywhere in our Ember app. However, we also want to be able to render certain parts (e.g., Ember components) in server-side views outside the Ember App.

Ember components are mainly made up of two files:

  • A logic file (app/components/[component name].js)
  • A view file (app/views/components/[component name]/hbs)

In many ways, we’re adding another view layer to our MVC app. Eventually, we’ll have enough components built that switching into a full-blown Ember frontend will make sense. The process allows us to better ease into that transition instead of sprinting into it full-force.

While we’re not going to walk through an entire project’s worth of decomposition, I do want to focus in on an example of a Post resource Index. First let’s take a peek at our controller and view logic on the Rails side of things:

app/controllers/posts_controller.rb

class PostsController < ApplicationController

  # Grabbing 10 most recent posts
  def index
    @posts = Post.all.order('updated_at DESC').limit(10)
  end

end

app/views/posts/index.html.erb

<h1>Posts</h1>
<% @posts.each do |post| %>
  <div class="post">
    <h2 class="title"><%= post.title %></h2>
    <div class="body">
      <%= post.body %>
    </div>
  </div>
<% end %>

As you can see, we’re listing all our Post objects into their own <div>. However, all of this logic is still server side. Our goal is to extract some of this functionality into the Ember side of things.

Think of this view like a cake. If the cake quality is pretty good, how we cut it won’t effect its overall taste. However, how we section up this cake will determine how equally the workload is distributed across our entire service ecosystem.

Option 1:

index.html.erb

<h1>Posts</h1>
<% @posts.each do |post| %>
  <div data-component='post-preview' data-attrs='{"post": "<%= post.as_json %>" }'></div>
<% end %>

Option 1 is technically more reusable since we might be able to find a place to use the post-preview section somewhere else in our app. However, its scope might cause some issues. For example, what if I wanted to do some Ember-side filtering of most posts’ data? Since we’re only focused on a single Post object, we’re unable to effectively interact with the @posts collection at large.

Option 2:

index.html.erb

<h1>Posts</h1>
<div data-component='recent-posts' data-attrs='{"posts": "<%= @posts.as_json %>"}'></div>

Option 2 seems to make the most sense for the context of what we’re trying to accomplish. We want to be able to interact with a collection of Post data on the client side (instead of server side).

Now that we have a good idea of how we want to decompose our initial view, let’s write the logic to do so!

Building Out the Component

Now that we’ve ironed out how we want to extract our server-side view, let’s build out the Ember component.

Let’s start with the component logic.

app/components/recent-posts.js

import Ember from 'ember';
const { Component } = Ember;

export default Component.extend({
  posts: null,

  init() {
    this.set('posts', this.get('posts');
  }
});

In our init() function, we’re setting the client-side posts variable to the server-side Post data that we’re passing into our component. Otherwise, our component is fairly bare bones. We want to start small with decomposition.

Next, let’s write our component template:

app/templates/components/recent-posts.hbs

{{#if posts}}
    {{#each post in posts}}
        <div class="post">
          <h2 class="title">{{post.title}}</h2>
          <div class="body">
            {{post.body}}
          </div>
        </div>
    {{/each}}
{{else}}
    <h2>Unable to find any recent posts!</h2>
{{/if}}

This view iterates on our original view with a few twists. Because of our component design, we’re checking to see if we have been passed any relevant posts. Otherwise, we’re simply translating the .erb Rails logic into Ember logic.

With all of these elements combined, we will have successfully created a bridge between our server-side (Rails) and our client-side (Ember) applications! While the two services are still more coupled than we would like them to be, developing this way allows us to slowly take small but effective steps toward completely pulling apart the view layer from our MVC monolith.

Next Steps in Decomposing Monoliths

From our current state, we have a world of possibilities before us.

In order to start leveraging the data storage aspect of things, there’s a need to create models and routes in the Ember view service. In Ember terms, we can modify our component to look like this:

app/components/recent-posts.js

import Ember from 'ember';
const { Component, inject } = Ember;

export default Component.extend({
  store: inject.service(),
  posts: null,

  init() {
    this.set('posts', this.get('posts');
  }
});

Injecting the store service allows us to leverage Ember datastore functions from within our component (e.g., this.get('store').[DS Method]). With this functionality, we’ll be able to split database calls between the Rails and Ember sections of our application.

Once you begin to feel comfortable with configuring an initial section of decomposition for your application, try breaking down more and more pieces of your view layer. This might take a little bit of time and tinkering to get used to, but you’ll eventually find yourself becoming more and more effective at view decomposition.

Reference: Decomposing Monolithic View Systems from our WCG partner Taylor Jones at the Codeship Blog blog.

Taylor Jones

Taylor Jones is a software chef at IZEA
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button