If you have ever worked with an object-oriented language, you have likely heard of object-oriented design (OOD). You may have even read the book by OOD guru, Sandi Metz, ‘Practical Object-Oriented Design in Ruby’ (POODR).

Here at LaunchPad Lab we strive to consistently manifest the primary principle Metz espouses: carefully designing your application to allow for flexibility to adapt to the scary unknown future, as easily as possible. One of the key corollaries of this principle is the ‘Single Responsibility Principle’ (SRP), to define methods and classes such that each one does one thing and one thing well (fulfills its purpose, cohesively). It should be an expert at that one thing and know little to nothing about anything else. This helps reduce the number of places you have to make changes should you wish to change or add functionality of your app. Often times that results in very short little methods, maybe just a couple lines long. Implementing this goal however, is no simple feat, and it requires diligent practice, and continuous vigilance.

Recently we discovered a prime opportunity to improve the flexibility of our very own website’s code, and drill into some POODR principles while we did so. Until now, the metrics widget in our About page was statically coded:

The numbers shown here have been hardcoded in the HTML, and may not have been updated since the page first launched. Since we’re constantly build new applications and contributing to the open-source community, these metrics need to be updated frequently. In order to keep the metrics current, we needed to make them dynamic based on real-time data about the work we’ve done and contributions we’ve made. Fortunately, making these numbers dynamic turned out to be an excellent exercise in POODR practice.

Project Metrics

Objective:

Develop code to calculate the following metrics:

Steps:

  1. Define the metrics; both the source and what each one constitutes
  2. Review the metric source, to understand how the data is provided and where the metric definitions fit in
  3. Develop code to achieve the objective in a working state
  4. Refactor code, with POODR principles in mind

As this post is meant to focus on implementing POODR principles, we’ll skip down to step 4 of the process, with the following context:

Sources:

Starting Point:

At first pass, we created one module, ProjectMetrics that held several methods:

You can probably guess what the first three methods did; each made its respective API call, grabbed the desired data out of the response, and returned the final metric (in our case a number). The last method essentially called each of those API methods and stored them in a hash, with their source as the key.

Obviously, this module does a lot of things, and knows about a lot of things. It knows how to get each API, and how to put the right APIs in a hash. More than that though, each individual method knows a lot too; each one knows how to make the API call (both the base, and appropriate query tags), how to sift through the data, and what final data point we want. This module had several responsibilities, with little flexibility should we want to ultimately add or change metrics (e.g., how many languages we code in, or what constitutes an in-house product).

POODR in Action

The first and easiest step would be to move the code for each API into its own module, and let ProjectMetrics worry only about putting together a hash of project metrics; it doesn’t care what those are. This may seem like overkill for a fairly straightforward request, but given the modifications considered above, the effort could pay off down the road.

For example, the Github module started like this:

require 'httparty'

module Github
  def self.num_of_repos
    header = {"User-Agent" => "Some-User"}
    data = HTTParty.get("https://api.github.com/search/repositories?q=org:launchpadlab+language:ruby", headers = headers)
    data["total_count"]
  end
end

This method feels fairly short and easy to follow, but as mentioned above it knows a lot of things. It knows how to make the API call, what the query is, and what should be returned. It’s a jack of all trades and not an expert in any one function. After applying POODR principles though, the Github module ended up looking quite different. There were more total lines of code, but this was due to introducing specialized methods, each of which were pretty short. The updated Github module is illustrated below.

Refactoring One Metric Source

We began by defining constants for the strings needed each time our Github API request is made. Defining constants improves the clarity of the code by explicitly defining the pieces of information that are static.

require 'httparty'

module Github
  BASE_URL = "https://api.github.com"
  HEADERS = {"User-Agent" => "Some-User"}

Next, we wanted a method to hold the kinds of queries we want to make. In our definition of open source projects, we determined they would be public LaunchPad Lab repositories in the Ruby language. Our available query params then looked like this:

def self.available_query_params
  {org: 'launchpadlab', language: 'ruby'}
end

Note, “public” is the default in the Github API, so we did not define it here. Essentially this method knows only a hash of key-values. The hash currently holds our current query params, so that this portion of the API request only needs to be changed in one place. This method could be refactored further to take optional arguments and simply create a hash, but for now, this is a good POODR improvement; if we want to change the query, we just need to change it in one place.

We then needed a method that could take a hash (our query params) and turn that into the Github API-friendly syntax. It doesn’t know anything about the Github API, or the query params (except that it’s a hash); this method only knows how to make a specific kind of string.

def self.create_query
  self.available_query_params.map { |k,v| k.to_s + ":" + v }.join(" ")
end

Of course, we need the actual method to make the Github API call, using the constants defined previously:

def self.search_for_repos
  HTTParty.get(BASE_URL + "/search/repositories", options)
end

Where options:

def self.options
  {query: {q: create_query}, headers: HEADERS }
end

We easily could have put that one line in the search_for_repos method. Perhaps there isn’t a lot of additional value to gain in this case, but it is not a bad idea to split this out in order to maintain flexibility in manipulating the options parameter in the future.

However, you will notice that “/search/repositories” is hard coded, but that’s why this method is called search_for_repos. Naming is an important facet of implementing the POODR principles and the SRP. The naming process helps you think about whether this method does more than what its name suggests. Furthermore, clear naming supports the “expressive coding” tenet of POODR: someone stepping into your app should be able to follow the code fairly easily, with little to no additional comments.

If we wanted to use the API differently we could build a separate method to define that part of the request and call it in a more agnostic api_ call method and use a variable there, or make a separate method that knows that part of the request for a different type of Github API call (e.g., “/users”).

Finally, we define a method that returns a specific data point, in this case the total_count, which represents the number. Again, this method knows a lot about what’s in the API call, but it doesn’t know how to make the API call, it simply expects a response with that key.

def self.num
  search_for_repos["total_count"]
end

Building the Project Metrics Hash

The other API modules followed a similar pattern, and all included a method called num, which helps us do a bit of duck typing in the ProjectMetrics module. The ProjectMetrics module now only knows how to create a project metrics hash based on inputs. Each method then is a building block to do so:

def self.available_metrics
  {num:
    {
      'Total Client <br />Applications' => 'Clients',
      'Open Source <br />Contributions' => 'Github',
      'In-House <br />Products' => 'Missions'
    }
  }
end

This method, as the name indicates, lists the available metrics. You will notice the first level keys represent the metric types (e.g., num, which show up as a method in each of the API modules. The value for that hash is another hash. Within the metric type hash, the keys represent the captions that appear on the About page. This was done to further reduce hard coding into the view, as well as ensure the appropriate values line up with the respective caption correctly. The values of this hash then represent the corresponding modules. It is easy now to add other categories to the num hash, or new metric type hashes.

From there we have a method that builds a similar set of hashes, except the values paired with the captions will reflect the result of the appropriate API call:

def self.build_metrics
  all_results = {}
  available_metrics.each do |metric_type, metric_hash|
    all_results[metric_type.to_s] = build_one_metric(metric_type, metric_hash)
  end
  all_results
end

Where the resulting hash would appear like this:

all_results = 
{num:
      {
        'Total Client <br />Applications' => 78,
        'Open Source <br />Contributions' => 25,
        'In-House <br />Products' => 17
      }
}

Within that method you can find build_one_metric:

def self.build_one_metric(metric, metric_hash)
  results = {}
  metric_hash.each do |metric_caption, metric_module|
    results[metric_caption] = metric_module.constantize.send(metric)
  end
  results
end

Again, this code could have been included within the build_metrics method, but the build_one_metric felt like a loop that could be used elsewhere, and might be valuable to have in a separate method.

In both methods you can see a lot of duck typing; these methods don’t know or care what metric names or types we want, they just knows how to put together a hash. We can then send this hash, or a specific key of this hash such as num, to the controller, and subsequently to the view to render on the page.

Throughout this process, you can see how asking yourself some of Metz’s basic questions “what does this method do?”, or “what does this method know?” can take you a long way in refactoring your code to improve flexibility. As with all things though, there is a constant balancing act among resource constraints and optimizing code and design. Hopefully this provided you with an example of how you could break down your code step by step, and implement sound OOD principles to improve your code.

Ifat Ribon

Developer

With a penchant for finding the best word in each situation, Ifat gravitated to coding as the perfect outlet for fitting the right words together. Ifat brings her consulting background, with a propensity for asking questions, to software development, and loves seeing the diversity of needs web applications can address. Away from her computer, Ifat is an avid lacrosse player. She runs a club team in Chicago and is always prepared to strike up a conversation about lacrosse or her team with anyone who will listen.

Ready to Build Something Great?

Partner with us to develop technology to grow your business.