If you've ever been working on an application with a domain concept like organizations I bet you had to struggle with custom features, behaviors and complete white labels. Most young fellows start such with the if-else construction which quickly can fall into monster-spaghetti. Can we do better?
Patterns to the rescue!
If you think for a while about the problem you should realize that it's nothing else but a perfect use case for Strategy Pattern - depending on the context (organization) you have to implement different strategies (features). Simple as that!
There are many different possible implementations, depending on the specific needs, project structure and a layer where different strategies may happen. I will skip the obvious cases (as you can find a lot of them on the web, starting from Wikipedia) and show you how to implement two different examples on the front-end using the standard Ruby on Rails setup.
Example no. 1: custom behavior
Imagine that you have a button that opens sign up form but one organization wants to integrate its users with their system (SSO or something) and click on the button should redirect to this system.
*The worst thing which you can do would look like this:
<% if @org.id == 42 %>
<%= link_to "Sign up with SSO", http://example.com/?magic_param=supersite" %>
<% else %>
<%= link_to "Sign up", sign_up_path %>
<% end %>
I've seen such code on production. Does it work? Sure and it would work for a long time if you won't touch this part at all never and ever. And some other parts, just to be sure. Unfortunately, sooner or later you will have to provide a different feature for another organization. Or for whatever reason, the organization id will change. Or you will change something somewhere else and this feature will stop working properly... the longer you think about it the more reasons to give up this path you will find.
As an alternative I propose five simple steps:
1. Add a field with custom features list to the organization
class AddCustomFeaturesToOrganizations < ActiveRecord::Migration[5.0]
def change
add_column :organizations, :custom_features, :hstore
end
end
2. Add a feature getter to the model
class Organization < ApplicationRecord
def version_of(feature)
custom_features.fetch(feature) { "default" }
end
end
3. Replace if-else
with dynamic loader for the required version of the feature
<%= render "features/sign_up_button/#{org.version_of("sign_up_button")}" %>
4. Split your feature into separate templates
# features/sign_up_button/_default.html.erb
<%= link_to "Sign up", sign_up_path %>
# features/sign_up_button/_monterail.html.erb
<%= link_to "Sign up with SSO", http://example.com/?magic_param=supersite" %>
5. Enable custom implementation for your enterprise customer
irb(main):042:0> org.custom_features["sign_up_button"] = "monterail"
irb(main):043:0> org.save!
What we achieved here is a pure separation between default and custom implementation, and a pretty simple path to introduce different custom solutions for any organization and any feature! To add another implementation for our example the only thing we need is to add one more template with a custom implementation, set a proper flag in a specific organization and it's done!
Example no. 2: Full white label
In most cases, white label equals custom template. Rails have a neat feature that allows us to extend loading path for templates so the solution is pretty simple - treat everything as default and when white label is enabled, prepend loading path with a custom directory for the organization. When Rails will find a file there it will be rendered otherwise it will take default one from the default path.
class ApplicationController < ActionController::Base
before_action :setup_white_label
def setup_white_label
prepend_view_path(["app/views/#{@org.white_label_name}"])
end
end
Just don't forget to fetch organization that should be used, probably based on a request URL.
When you combine this two simple techniques and the pattern for your classes you can easily create fully customizable applications that will support your enterprise customers needs.
Confidently build your next app
Our devs are so communicative and diligent you’ll feel they are your in-house team. Work with experts who will push hard to understand your business and meet certain deadlines.