Posted 2 days back at GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home
Those of you coming from a Google search are about to be disappointed: this is a post about types of coupling in programming.
Coupling refers to the degree to which components in your program rely on each other. You should generally seek to minimize this property, though you’ll see it’s impossible to eliminate entirely.
Here are a few types of coupling, ordered by severity (first is worst):
Pathological Coupling
Your class reaches inside another class and reads (or, perish the thought, changes) its instance variables.
You are literally pathological and deserve the pain this will cause you.
class NuclearLaunchController
def initialize(launch_codes)
@launch_codes = launch_codes
end
end
class ExtremelyBadIdea
def initialize(nuclear_launch_controller)
@launch_controller = nuclear_launch_controller
end
def do_bad_things
# This is poison
@launch_controller.instance_variable_set(:@launch_codes, 'password')
end
end
Global Coupling
You have two classes that both rely on some shared global data, maybe a Singleton or a class variable.
When many test files all depend on global factory definitions, a change to any one can ripple through the system.
# spec/factories.rb
FactoryGirl.define do
factory :user do
# Changes here are global and can affect many test files.
end
end
# spec/model/user_spec.rb
before do
# This refers to global data.
@user = build_stubbed(:user)
end
# spec/model/order_spec.rb
before do
# So does this.
@user = build_stubbed(:user)
end
Note that this is probably an example where the cure (duplicating the logic for creating test objects in every spec) is worse than the disease.
Control Coupling
You pass in a flag that tells a method what to do.
Remember save(false) in ActiveRecord? That boolean argument caused control coupling. Remember how we all had to change our code when it became save(validate: false)? If we’d been calling save and save_without_validation instead, Rails Core could have refactored that method more times than the router and we’d never have known. Also, notice that passing validate: false into save does not reduce the coupling, it’s just disguised better.
Control couples are smelly because the calling method has intimate knowledge of how the receiver implements the method being called. You’re determining what an object should do based from outside it. Good OOP lets objects decide what to do based on their own state.
def save(should_run_validations=true)
# When you see a parameter in a conditional, that's control coupling.
# The fact that this method has an if in it has leaked out into client code.
# Changes can now require changes in these clients.
if should_run_validations
run_validations
persist
else
persist
end
end
# One possible fix: define two methods and let the clients
# choose which to call. Now we can refactor either without
# affecting clients.
def save
run_validations
persist
end
def save_without_validations
persist
end
Data Coupling
You call a method and pass it a parameter that doesn’t affect its control flow.
This is still coupling, but we’re starting to reach the kind that isn’t so bad. Sometimes you need parameters! If you wanted to remove all coupling you wouldn’t be able to pass data between objects at all.
class ScreenPrinter
# This method is coupled to its parameter, because a change to that argument
# can cause breakage (if we undefined to_s, for example).
def print(text)
output_to_screen(text.to_s)
end
end
Message Coupling
You call a method on an object and send no parameters.
You’re coupled to the name of the message, but not any hint of its implementation. This is the loosest type of coupling and should be your goal. Notice that this makes methods that take no arguments better than methods that take one (and so on).
# No reliance on anything outside this object. Feels good, man.
class ScreenPrinter
def print_to_screen
output_to_screen(@text)
end
end
Keep an eye out for the nasty types of coupling in your code, and see if you can can’t refactor it into something further down the ladder.
(I cribbed this list of coupling types from Wikipedia’s article, paraphrased and added examples. The original article is worth reading.)
Posted 2 days back at Ruby5
We look at easier schema-less hstore on Postgres, Ruby versions in your Gemfile, and Skype in your app. We learn how to DRY better, build a Gem from scratch, build a book without scratching yourself and how to binge on Code School for free.
Listen to this episode on Ruby5
This episode is sponsored by Harvest
Harvest is a painless time tracking and invoicing Rails application relied on by the most innovative teams in over 100 countries worldwide. Track time from anywhere, and invoice your clients in seconds.
meta_types
meta_types from the metaminded team is a clean implementation for using hstore (schema-less data) on postgres, which is supported on Heroku.
Multiple Ruby version support on Heroku
Heroku now allows you also specify what version of Ruby you want to run in your Gemfile. The only caveat is that you can't specify a patch level. Heroku will pick the safest patch level for a specific version.
Libskypekit & Skypekit
Skype, the VoIP application has a developer API that will allow you to interface with the native desktop Skype app, and another one that uses the SkypeKit runtime to basically giving you a headless Skype application.
You can program it to make or receives calls or instant messages through the Skype platform and setup video conferencing. Andriy Yanko from Railsware just released a Gem using Foreign Function Interface (FFI) to communicate with SkypeKit so you can build apps using Skype.
Code Duplication
In an interesting series of blog posts on Code Smells, Piotr Solnica talks about code duplication and how it’s a bit more complex than just finding redundant code. Through a few examples he demonstrates that it’s often duplicated concepts in your codebase that you should target when refactoring. He also presents cases where reducing duplication causes added complexity and obfuscation, which can be worse.
From customer requirements to releasable gem
Ken Mayer dropped us a line about a blog post he wrote on Sunday where he walks through extracting some functionality from a client project into open source. While the library itself is pretty simple, allowing you to set attributes on a model to read only on a per-instance basis, the blog post itself is a great case study on all of the steps you should go through to develop a well rounded piece of open source: giving the code a MIT license, a gem specification, documentation in the README, integration tests, Generators, and shareable tests.
Build a book with Bookshop
We often encourage people to contribute to open source projects on Ruby5 but there’s another great way to help improve the community: writing books. Putting digital pen to binary paper. Dave Thompson let us know about a gem he just released called Bookshop which serves as an open-source book development framework. You can write everything in HTML5, CSS and JavaScript and Bookshop will help you output to any of the common eBook formats: Print or Online PDF, mobi, and ePub.
Code School Free Weekend
This weekend from Friday, May 18th at 8 PM to Sunday, May 20th at midnight eastern time everything on Code School will be completely free.
If you’ve been meaning to check out some of our paid Code School courses, like Backbone.js, CoffeeScript, Rails Best Practices, CSS Cross Country, Rails Testing for Zombies, or Journey Into Mobile this would be a great opportunity to give it a try.
Spots for the weekend are limited, so to reserve a space you’ll want to follow the link below.
Posted 2 days back at mir.aculo.us - Home
There are no excuses to not have fast-loading, delightfully quick websites and apps. With Amy Hoy’s and my JavaScript Performance ebook, you’ll learn about what’s important and where the low-hanging web performance fruit hangs; as well as how to speed up your JavaScript codes—and for a short 4 days (until May 19!) we’re selling it for more than 50% off with code JSSPRING.
Grab your copy now for just $19! $39

Posted 2 days back at InfoQ Personalized Feed for unregistered user - Register to upgrade!
Mark McGranaghan presents how Heroku has designed, developed and operated cloud services providing high availability for their PaaS. By Mark McGranaghan
Posted 2 days back at GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home
FactoryGirl 3.3.0 was released this weekend with a slew of improvements.
To install, add (or change) your Gemfile:
gem 'factory_girl_rails', '~> 3.3.0'
New Callback Syntax
Callbacks have been revamped to work well in conjunction with custom
strategies. Instead of declaring callbacks like this:
FactoryGirl.define do
factory :user do
factory :user_with_posts do
after_create {|instance| create_list(:post, 5, user: instance) }
end
end
end
you can declare callbacks with before and after, passing the symbol of
the callback as the name:
FactoryGirl.define do
factory :user do
after(:custom) {|instance| instance.do_something_custom! }
factory :user_with_posts do
after(:create) {|instance| create_list(:post, 5, user: instance) }
end
end
end
Finally, you can use completely custom callbacks without a before or after
prepended by just calling callback:
FactoryGirl.define do
factory :user do
callback(:custom_callback) {|instance| instance.do_something_custom! }
end
end
These work great with custom
strategies:
class CustomStrategy
def initialize
@strategy = FactoryGirl.strategy_by_name(:create).new
end
delegate :association, to: :@strategy
def result(evaluation)
@strategy.result(evaluation).tap do |instance|
evaluation.notify(:custom_callback, instance) # runs callback(:custom_callback)
evaluation.notify(:after_custom, instance) # runs after(:custom)
end
end
end
Support all *_list methods
FactoryGirl already introduced build_list and create_list to build and create
an array of instances; in 3.3.0, *_list methods are generated dynamically
for all strategies registered, so build_stubbed_list and
attributes_for_list join the immediate roster of methods; if you were to
register a strategy named “insert”, insert_list would exist as well.
Fix to_create and initialize_with within traits
Traits are a great way to name an abstract concept of attributes, but for a
long time, they didn’t support defining to_create or initialize_with.
3.3.0 fixes this shortcoming by having to_create and initialize_with
behave in traits exactly as you’d expect. This is perfect for decorating
objects from within FactoryGirl.
class NotifierDecorator < BasicObject
undef_method :==
def initialize(component)
@component = component
end
def save!
@component.save!.tap do
Notifier.new(@component).notify("saved!")
end
end
def method_missing(name, *args, &block)
@component.send(name, *args, &block)
end
def send(symbol, *args)
__send__(symbol, *args)
end
end
FactoryGirl.define do
trait :with_notifications do
to_create {|instance| NotifierDecorator.new(instance).save! }
end
factory :user
end
create(:user, :with_notifications) # decorates save! when the instance is created
FactoryGirl.define do
trait :with_notifications do
initialize_with { NotifierDecorator.new(new) }
end
factory :post
end
create(:post, :with_notifications) # returns a post instance decorated with NotifierDecorator
Define to_create and initialize_with globally
If you’re using an ORM other than ActiveRecord, you may want to call different
methods for persistence. Declaring a to_create (or initialize_with, if you
wanted to use a global decorator) within the FactoryGirl.define block will
now apply to all declared factories, behaving much like sequences, traits, and
factories.
You can override the global to_create or initialize_with with traits or by
defining to_create in a factory explicitly.
FactoryGirl.define do
to_create {|instance| instance.persist! }
factory :user do
factory :user_backed_by_active_record do
to_create {|instance| instance.save! }
end
end
end
What’s next?
There are still cases where traits don’t behave correctly (using implicit
traits is a big remaining bug) and more work for initialize_with and
accessing attributes needs to be done.
Posted 3 days back at Jay Fields Thoughts
If you've ever spent any time learning Rails then you probably read one of the editions of Agile Web Development with Rails, and if you're like me (skeptical & pedantic) then you probably asked yourself: what the hell does Rails have to do with
Agile development? At the time, I assumed that Dave and David were merely capitalizing on the buzz around Agile; however, even if that's the case, I think they did manage to highlight one of my favorite aspects to building websites with Rails: The ability to make a change, reload the page and see the results makes you a much more agile programmer - where 'agile' is defined as: Characterized by quickness, lightness, and ease of movement; nimble
It turns out, it's not very hard to get that same productivity advantage in Clojure as well. I would go so far as to say that the ability to change the server while it's running is assumed if you're using emacs+slime; however, what's not often mentioned is that it's also possible (and trivial) to reload your server code (while it's running) even if you're using IntelliJ, scripts, or anything else.
The majority of the servers I'm working on these days have some type of web UI; therefore, I tie my server side code reloading to a page load. Specifically, each time a websocket is opened the server reloads all of the namespaces that I haven't chosen to ignore. The code below can be found in pretty much every Clojure application that I work on.
(defonce ignored-namespaces (atom #{}))
(defn reload-all []
(doseq [n (remove (comp @ignored-namespaces ns-name) (all-ns))]
(require (ns-name n) :reload )))
Like I said, when I open a new websocket, I call (reload-all); however, the (reload-all) fn can be called on any event. When discussing this idea internally at DRW, Joe Walnes pointed out that you could also watch the file system and auto-reload on any changes. That's true, and the important take-away is that you can easily become more productive simply by finding the appropriate hook for what you're working on, and using the code above.
The ignored-namespaces are important for not reloading namespaces that don't ever need to be reloaded (user); other times you'll have a namespace that doesn't behave properly if it's reloaded (e.g. I've found a record + protocol issue in the past, so I don't dynamically reload defrecords in general).
The change-reload webpage-test loop is nice for making changes and seeing the results very quickly - and I strongly prefer it to having to stop and start servers to see new functionality.
Posted 3 days back at InfoQ Personalized Feed for unregistered user - Register to upgrade!
James Pearce discusses the status of HTML5, what it can do today and what it still missing across major mobile browsers. By James Pearce
Posted 3 days back at InfoQ Personalized Feed for unregistered user - Register to upgrade!
In this interview with at QCon London, LinkedIn’s Sid Anand discusses the problems they face when serving high-traffic, high-volume data. Sid explains how they’re moving some use cases from Oracle to gain headroom, and lifts the hood on their open source search and data replication projects, including Kafka, Voldemort, Espresso and Databus. By Siddharth Anand
Posted 3 days back at InfoQ Personalized Feed for unregistered user - Register to upgrade!
Brendan Eich recaps the major milestones and controversies in JavaScript’s history, the performance improvements, the current work on the next version of JavaScript, ending with some demoes. By Brendan Eich
Posted 3 days back at igvita.com
Whenever the point I'm trying to make lacks clarity, I often find myself trying to dress it up: fade in the points, slide in the chart, make prettier graphics. It is a great tell when you catch yourself doing it. Conversely, I have yet to see a presentation or a slide that could not have been made better by stripping the unnecessary visual dressing. Simple slides require hard work and a higher level of clarity and confidence from the presenter.
All presentation software is broken. Instead of helping you become a better speaker, we are competing on the depth of transition libraries, text effects, and 3D animations. Prezi takes the trophy. As far as I can tell, it is optimized for precisely one thing: generating nausea.
Next Presentation Platform: Browser
If you want your message to travel, then the browser is your (future) presentation platform of choice. No proprietary formats, no conversion nightmares, instant access from billions of devices, easy sharing, and more. Granted, the frameworks and the authoring tools are still lacking, but that is only a matter of time.
Unfortunately, we are off to a false start. Instead of trying to make the presenter more effective, we are too busy trying to replicate the arsenal of useless visual transitions with the HTML5, CSS3 and WebGL stacks. Spinning WebGL cubes and CSS transitions make for a fun technology demo but add zero value - someone, please, stop the insanity. We have web connectivity, ability to build interactive slides, and get realtime feedback and analytics from the audience. There is nothing to prove by imitating the broken features of PowerPoint and Keynote, let's leverage the strengths of the web platform instead.
Free Lunch: Web Analytics
Keynote, PowerPoint and friends are optimized to help hide your incompetence: no useful feedback, no way to measure the effectiveness of your delivery or reach of the message. On the web, we inherit all of the power of web analytics for free. We can measure slide impressions, time on slide, referrals, clicks, display heatmaps, segment the viewers, setup conversion goals and more. Let's look at a real-life example.
I've instrumented my RailsConf presentation (Making the Web Faster) with Google Analytics, where I'm tracking slide transitions and clicks via custom events and time on slide via the user timings API:

7.5K+ visits, 200K+ slides impressions, and a 40%+ return rate. An average user took 19 minutes to make their way through the slides, which translates to just over 100 "cognitive days" across all visitors. It took me roughly 20 hours to make the slides from scratch, which translates to a 1:120 hour ratio. Is this good? We can't say, but it is a baseline. I would love to compare these numbers to other RailsConf presentations.
Vanity counters are fun to share, but did the presentation convey the right message? The goal was to focus the audience on optimizing for user perceived latency and the available tools. The peaks in time on slide up to slide 20 correspond to off-site clicks to Navigation Timing spec, Google Analytics documentation, and examples - mission accomplished. Slides 30 to 40 are mostly flat: I need do a better job of motivating webpagetest.org, because it is an amazing tool. Finally, the big timing spike at the end corresponds to slides on mod_pagespeed. Next time around I will make sure to spend more time on it.
Video & YouTube Analytics

The presentation was recorded and I uploaded it to my own YouTube account - this gives you a lot of great analytics. First, it is reassuring to see that web visitors and YouTube audience retention follows the same pattern, with many of the same peaks and valleys. The added bonus: I can click on any time point and review my delivery. And what's the retention peak of entire presentation? An "inception" demo of opening Chrome's inspector, on the inspector! Not surprisingly, this also corresponds to the reaction of the audience during the live presentation.
Make me a better presenter, please!
If I was to give the same presentation again given the information above, I am confident I could now do a better job of it. Except, of course, I wouldn't give the same presentation since I can clearly see the sections that need to be improved, and a few sections that need to be cut.
I would love to see some experiments with live session feedback: am I going too fast, should I revisit a concept, perhaps even live questions. Add a websocket endpoint and all of the above is easily done. This space is ripe for disruption. Forget the animations, ornate templates, and other me-too gimmicks, make me a better presenter.
Posted 4 days back at GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home
When you name a class, make sure to choose something that is unlikely to refer to two different things.
For example, Visitor is a bad name for a class that represents unregistered users of your web site.
This issue is that Visitor is the name of a well-known pattern. Like it or not, the Gang of Four have claimed this one (that’s why they’re called a gang).
If you use this class name in a web-app, I can make a pretty good guess which concept you’re referring to, but there’s too much ambiguity. The name has synonymatic complexity: it sounds too much like another thing.
UnregisteredUser is a much better name. It’s easy to guess what this refers to. However, notice that the ambiguity is affected by your problem domain: if we were writing an event-registration app, this name would again be ambiguous.
In general, choose names that would make it easy for a colleague to correctly guess an object’s identity.
Posted 5 days back at GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS - Home
We haven’t been taking pull requests on shoulda-matchers as much as we shoulda, but it’s back in the spotlight now. Shoulda’s been gone … but it’s back.
<iframe frameborder="0" height="315" src="https://www.youtube.com/embed/CNXDCHHjEvs?rel=0" width="420"></iframe>
Here are the new features in shoulda-matchers 1.1.0:
- A NEWS file so you can track new hottness in REAL TIME
- shoulda-matchers’ very first dependency: ActiveSupport >= 3.0.0. This was an implicit dependency for a while, but now it’s official.
- An
only_integer option for validates_numericality_of
- A
query_the_database matcher, so that you can do it { should query_the_database(4.times).or_less }
- Database column primality is correctly checked
- The flash matcher can check specific keys using
[], like so: it { should set_the_flash[:alert].to("Bad username") }
- A
validates_confirmation_of matcher: it { should validate_confirmation_of(:password) }
- A
serialize matcher: it { should serialize(:details).as(Hash).as_instance_of(Hash) }
- A
have_sent_email matcher can now check reply_to: it { should have_sent_email.reply_to([user, other]) }
- An
accept_nested_attributes matcher
If you filed a pull request and we didn’t take a look, please re-submit or ping me on Github. We’d love to hear from you.
Posted 6 days back at
A few months ago, I started to invest heavily in Chef to automate the roll
out of our applications and the supporting infrastructure. So far, so good but it has not always been sunshine and
puppy dogs. One of the major challenges is attempting to reuse cookbooks found on the
community site, on GitHub or even within our own organization. I have
found that I frequently had to customize the cookbooks heavily or rewrite the cookbooks from scratch to meet our
needs.
Recently I have discovered a pattern that we use in our internal cookbooks that seems to make reuse possible, even
easy. So I thought I would send it out into the world to see if it is something that others would find useful.
So here is how it evolved...
Phase 1: Cookbook as a big bash script
In the beginning, our cookbooks mostly felt like big bash scripts. Conceptually they would do something along the
lines of;
bash "install mypackage" do
cwd "#{Chef::Config[:file_cache_path]}"
code <<-EOH
wget http://example.com/mypackage-1.0.tar.gz
tar xzf mypackage-1.0.tar.gz
cd mypackage-1.0
./configure && make && make install
EOH
not_if { File.exists?("/usr/bin/mypackage") }
end
This was fast to write but that is the best that could be said about this technique. This approach resulted in no
reusability of cookbooks unless we had the exact same requirements on a different node.
Phase 2: Attributes to customize
We quickly ran into issues when we needed to customize the application based on the environment. At which point we
introduced attributes to customize the application. Conceptually, our recipes started to look something like;
bash "install mypackage" do
cwd "#{Chef::Config[:file_cache_path]}"
code <<-EOH
wget http://example.com/mypackage-#{node[:mypackage][:version]}.tar.gz
tar xzf mypackage-#{node[:mypackage][:version]}.tar.gz
cd mypackage-#{node[:mypackage][:version]}
./configure && make && make install
EOH
not_if { File.exists?("/usr/bin/mypackage") }
end
template "/etc/mypackage.conf" do
source "mypackage.conf.erb"
mode "0644"
variables(
:database => node[:mypackage][:database],
:user => node[:mypackage][:user],
:password => node[:mypackage][:password]
)
end
Phase 3: Partition the recipes into units of reuse
Further down the track we found that different nodes would have different requirements. i.e. One installation of
mypackage would use a local database for authentication while another installation would authenticate
against Active Directory. This resulted in us splitting recipes into multiple recipes based on the units of reuse.
So our hypothetical "mypackage::default" recipe would be split into "mypackage::default", "mypackage::db_auth",
"mypackage::ad_auth". The role would include the particular recipes that it required.
Phase 4: Resources to the rescue
Resources (via LWRPs) were the next abstraction that we introduced. This made it easy to repeat similar sets of
complex actions in many recipes with minor differences in configurations. A typical scenario involves defining
multiple queues in a message broker, such as this snippet using the
glassfish cookbook;
glassfish_mq_destination "WildfireStatus queue" do
queue "Fireweb.WildfireStatus"
config {'validateXMLSchemaEnabled' => true, 'XMLSchemaURIList' => 'http://...'}
host 'localhost'
port 7676
end
glassfish_mq_destination "PlannedBurnStatus queue" do
queue "Fireweb.PlannedBurnStatus"
config {'maxCount' => 1000, ...}
host 'otherhost'
port 7676
end
Phase 5: Data driven reuse
The use of resources allowed us to easily create customized cookbooks but authoring the cookbooks could get
monotonous. There was a lot of boilerplate code in each recipe. We reacted by storing storing a simplified
description of the resources as data, interpreting the description and invoking the resources to represent the data.
Sometimes the description was stored in data bags, sometimes the description was synthesized by searching the chef
server, sometimes the description was synthesized using a rule layer.
For example, we discovered the set of queues to create in our message broker by searching the chef server for
nodes in the same environment that declared a requirement for message queues in the attributes (i.e.
"openmq.destinations.queues"). When configuring the logging aspects of our systems, we search for a
graylog2 node and ensure we get the production node in the production environment and the development node
in all other environments.The .war files and their required customizations are declared in a data bag and we query the
data bag when populating our application server.
Phase 6: Policy recipe + Attribute driven recipe
The data driven approach saved us a lot of work but it limited the amount of cookbook reuse; business rules were
encoded into the the way we stored, synthesized and discovered the data. It also meant that some of our core
cookbooks changed every time we changed the way we abstracted our application configuration data.
Our most recent approach has been to pull the the business specific policy code out into a separate cookbook and
then include a recipe that uses the attributes defined on the current node to drive the creation of the
infrastructure.
Our policy cookbooks tend to look something like the following.
node.override[:openmq][:extra_libraries] = ["http://example.org/repo/myext.jar"]
queues = []
search(:node, 'openmq_destinations_queues:* AND NOT name:' + node.name) do |n|
queues.merge( n['openmq']['destinations']['queues'].to_hash )
end
queues.merge( node['openmq']['destinations']['queues'].to_hash )
include_recipe "glassfish::attribute_driven_mq"
This approach seems to have given us a way to create a reusable cookbook (
glassfish in the case above) with the components
that are less likely to be reused in a separate "policy" recipe. We are already using this to successfully manage an
application server, a message broker, to configure monitoring and logging and to apply firewall rules.
I wonder if this is an approach that others have discovered and if it could be applied to other cookbooks.
Posted 6 days back at Ruby5
Bootstrapping young JS framework released, poppin BubbleWrap, internationalizing with alchemy, mixins and refactoring, hanging out at Rpub, and a rack middleware for contact importing.
Listen to this episode on Ruby5
This episode is sponsored by Harvest
Harvest is a painless time tracking and invoicing Rails application relied on by the most innovative teams in over 100 countries worldwide. Track time from anywhere, and invoice your clients in seconds.
HTML 9 Responsive Boilerstrap JS
The latest JavaScript hotness, cross-universe compatible.
i18n Alchemy
Internationalizing dates and currency is a snap with i18n Alchemy from the Ruby Mendicant University.
BubbleWrap
A collection of wrappers and helpers for RubyMotion.
Mixins: A refactoring anti-pattern
Steve Klabnik on mixins, refactoring, and code complexity.
Rpub
An ePub generator in Ruby.
OmniContacts
A generalized Rack middleware for importing contacts from major email providers.
Posted 7 days back at entp hoth blog - Home
While we were trying to scope a new Tender feature, we accidentally stumbled across a very simple and effective UI improvement that we were able to implement, test and deploy in the space of an hour. I figured I could introduce this (very small) feature with the (very short) story of how we built and shipped it.
The issue was thus: We provide per-queue notification settings for support staff. These only affect whether the staff member in question receives email notifications for a queue. However, these notification settings also represent something else. They’re a list of queues that are important or relevant to a particular staff member. Amazingly, we don’t make use of this information anywhere else! I know!
As things wound down in the ENTP office a couple of Fridays ago, Courtenay and I became so energized by this discovery. Courtenay immediately pushed his first pass at a topic branch for this feature. I reviewed and staged it. We went over the QA routine ourselves, since everyone else had mostly gone for the weekend. There was some confusion at first because I couldn’t find the right notification settings. This is why I don’t usually do QA. We pressed on, because we are intrepid and we kept the scope very small. We verified that it worked and shipped it to production to see it in action on our own support page.
Within an hour we had the following simple, new additions:

From now on when you elect to be notified about a queue, that queue will receive priority in the support dashboard UI under “My Queues”. Since we were keen to get a useful change shipped right away, we didn’t do any more than this for the first pass.
I’m not going to lie, we were already barfing rainbows over this, so great was our excitement at this easy win. But! There was one very important bit of functionality missing, in my opinion: What I really wanted was the ability to see discussions from all of my queues in the pending view at once. So we took a little more time and added that too.

We left ourselves greater leeway on the turnaround for this one, and we were able to get our usual QA approval before shipping. We were even able to pay off some technical debt in related code at the same time! This change has been very important to me personally as I find it to be conducive to my own workflow. It gives me an easy way to check whether any queued discussions that I should know about about are waiting for a response, and nothing else.

Like I said it’s the small things, or maybe we here at ENTP are hopeless workflow nerds. Possibly both.