multipurpose knife

The CBITs web developers have decided to focus on a new programming principle, heuristic or pattern each week. Our hope is that by doing so, we will start to develop a more intuitive understanding of the tradeoffs involved when applying these principles.

For our first week, we have chosen the Single Responsibility Principle. The tl;dr version is: each unit of code should have a closely related set of features that serve a unified goal. Below we will get into specific examples that illustrate this.

Example 1: Refactoring opportunity

This first example comes from a Cordova application called MedLink, built with JavaScript on top of the Backbone library. It involves a model "class" that has outgrown its original responsibility of storage and retrieval of application configuration settings. It is now being used to access the API of a native Android application called Purple Robot. As configuration settings are updated within the host application, remote methods are invoked on Purple Robot.

function Settings() {
  ...
  this.setMedicationTime = function(time) {
    localStorage[KEYS.MEDICATION_TIME] = time;
    this._updateMedPromptTriggers();
  };
  ...
  this._updateMedPromptTriggers = function() {
    ...
    // add a nightly trigger to run about midnight and reset the tray notification and adherence value
    purpleRobotService.updateTrigger({
      ...
    }).execute();
  ...
}

This code is ripe for refactoring. It no longer has a single reason to change, but at least two: 1. when there are updates to configuration options and 2. when the desired behavior of Purple Robot changes. One possibility is that event listeners could be added so that when a relevant setting changes, a unit of code that contains all Purple Robot programming logic would be notified and perform the necessary API calls. For example:

function Settings() {
  ...
  this.setMedicationTime = function(time) {
    localStorage[KEYS.MEDICATION_TIME] = time;
    this.trigger("medicationTimeChange", time);
  };
  ...
}
...
function PurpleRobotRules() {
  function updateMedPromptTriggers(time) {
    ...
  }
  this.initialize = function(settings) {
    settings.on("medicationTimeChange", updateMedPromptTriggers);
  };
}

Example 2: Working with localStorage

This example was an exercise in getting to know localStorage. Using HTML5 and JavaScript, we've created a simple CRUD document that generates a random array of integers. The user can then store the array into the browser's storage and also display the stored array. Once it has been stored, the user can choose to delete an integer using the drop down or delete the entire array and start fresh.

var array, arrayPosition, storedArray;
var arrayMax = 8;
var dropdown = document.getElementById('arraySelector');
function generateNumbers() {
    array = [];
    while (array.length < arrayMax) {
        var randomInteger = Math.ceil(Math.random()*10);
        array.push(randomInteger);
    }
}
function storeNumbers() {
    localStorage.setItem('numbers', JSON.stringify(array));
    storedArray = JSON.parse(localStorage.numbers);
}
function showNumbers() {
    var div = document.getElementById('numberDisplay');
    div.innerHTML = localStorage.getItem('numbers');
}
function deleteSingleNumber() {
    arrayPosition = dropdown.options[dropdown.selectedIndex].value;
    array.splice(arrayPosition,1);
    storeNumbers(); //update localStorage with shortened array
    showNumbers(); //automatically refresh the displayed array
}
function deleteNumbers() {
    localStorage.clear();
    showNumbers();
}

The functions are then called using HTML buttons on the document body. You can see a rendered preview here: http://jsbin.com/julonici/2/

Example 3: Rails modules

Rails sometimes makes it difficult to adhere to the SRP. ActiveRecord models inherently violate the Single Responsibility Principle. Business logic that may involve the interaction of more than one model is often moved into concerns: modules that aid in the splitting up of complicated model interactions. These concerns attempt to bridge the gap between non-SRP Rails conventions and classes that handle a single conceptual piece of domain logic.

One fairly common feature CBITs applications must often accommodate deals with the determination of a patient's adherence status to the BIT. Here is an example of one particular implementation of this that attempts to maintain the SRP while at the same time not cluttering up the Participant model class with a logically distinct set of functions.

# Handles Participant overall status logic
# Module is included in the Participant model
module Status
  extend ActiveSupport::Concern
  def current_study_status
    if start_date
      if current_lesson && previous_lesson
        two_lessons_passed
      elsif current_lesson
        one_lesson_passed
      else
        "disabled"
      end
    else
      "none"
    end
  end
  private
  def current_lesson
    Lesson.where("day_in_treatment <= ?", study_day)
          .where(locale: locale)
          .order(day_in_treatment: :desc).second
  end
  def previous_lesson
    Lesson.where("day_in_treatment <= ?", study_day)
          .where(locale: locale)
          .order(day_in_treatment: :desc).offset(2).first
  end
  def current_lesson_complete?
    if current_lesson
      current_lesson.content_access_events.where(participant_id: id).any?
    end
  end
  def previous_lesson_complete?
    if previous_lesson
      previous_lesson.content_access_events.where(participant_id: id).any?
    end
  end
  def two_lessons_passed
    if  !current_lesson_complete? &&
        !previous_lesson_complete?
      "danger"
    elsif !current_lesson_complete? ||
          !previous_lesson_complete?
      "warning"
    else
      "stable"
    end
  end
  def one_lesson_passed
    if current_lesson_complete?
      "stable"
    else
      "warning"
    end
  end
end