Client-Side Separation of Concerns: Are We Doing It Wrong?

Separation of Concerns is a programming principle that encourages grouping functionality in ways that reduce overlap. 'Concern' is loosely defined - Wikipedia describes it as 'any piece of interest', like a behavior or piece of functionality. The core concept is that by improving the separation of concerns, you improve a program's clarity and durability. Actual separation is done in three ways:

  • Encapsulation — bundling the concern as a single idea, in a single place
  • Modularity — making concerns portable and independent
  • Information Hiding — smoothing concern interaction by providing interfaces between them

Using these methods, you increase the separation of your concerns, and strengthen your system. Easy enough in programming, but how do we apply this principle when working on the client side — that is, CSS/HTML/JS?

I always thought I knew, but in the past few years, some new challenges caused me to rethink the approach. To explain, I'll start by examining the most common SoC model: Content, Presentation, Behavior.


The Classics: CPB

If you haven't heard of the SoC principle, you've probably heard of the three 'layers' of Content, Presentation, and Behavior. These are the three classic concerns that front-end developers are taught to keep separate. How to separate them is another matter, owing to some interpretation. Here's the version I learned:

  • The concerns are divided into their own files (and languages)
  • The content (markup) is dumb — that is, style- and behavior-agnostic. Usually, this means writing it first, and only minimally changing it to accommodate JS and CSS when necessary.
  • Classes are content-oriented, not presentational ('additional-info' vs. 'right-col')
  • Elements are styled using clever selectors and the cascade
  • JavaScript is unobtrusive — it selects, operates on, and injects markup to aid behaviors

This should look familiar; it's the classic 'best practice' for front-end work. Comparing it with the first three bullets about SoC, how does it stack up?

  • Encapsulation — check. Concerns are clearly encapsulated.
  • Modularity — arguable. While there's a degree of portability (see CSS Zen Garden), it depends on having a very consistent markup structure, which brings us to...
  • Information Hiding — the big missing piece with the classic, 'markup-first' separation of CPB. Dumb markup, markup that's written solely for content and not for presentation or behavior, doesn't provide an interface. By doing this, it simply offloads the complexity to CSS and JS, which have to parse and select it in clever ways. This leads to all the problems SoC is supposed to prevent — brittleness, overrides, excessive coupling.

This issue — exactly how to separate content, presentation, and behavior — is at the heart of new CSS strategies like OOCSS and SMACSS. The traditional wisdom that dumb markup is good markup is under seige.

So, maybe we don't know exactly how to separate our concerns. But let's step back — why are content, presentation, and behavior our primary concerns in the first place?

Are they the right ones?

CPB is certainly an appropriate concern set for documents, where the content is core and CSS/JS just 'theme' it. This problem domain is what HTML was designed for, and it's what most of us started our careers writing. The question, though: is CPB is right for a Rails site with 100+ views, an all-JS chat application, an online store, or a web-only game?

If not CPB, then what? To take a cue from programming, maybe the right concerns are simply the aspects of your buildout, and your task should be to determine those aspects and how best to separate them. Once you understand your concerns, the task is not how to separate them along the CSS/JS/HTML lines, but to separate them from eachother.

Easier said than done, right? How do we identify concerns, and how do we detangle them? The rest of this post is just that — some loose definitions of common concerns, and some very stripped-down examples of how a dev could improve their separation.


New concerns

Layout

Blueprint, now 5 years old, was one of the first examples of client-side devs addressing a new concern — Layout. It's still one of the easiest concerns to abstract out from your build, and Blueprint has numerous competing frameworks that accomplish the same goal. The concept is simple: using presentational, layout-only classes separates the problem of visually structuring your design from the task of visually styling it.

Unseparated:

In this example, the article type dictates a layout behavior for the additional-info box. In some cases, this kind of behavior needlessly entangles two concerns that aren't otherwise related.

 .news-article .additional-info,
 .blog-article .additional-info {
 background: url('more-news.png');
 float: right;
 width: 320px;
 }
Separated:

By definining a 'layout' to control this behavior, we can separate this responsibility more clearly. This makes the layout itself more portable, and cleans up the article-specific styles.

 .wide-layout .aside-col {
 float: right;
 width: 320px;
 }

Page

What page is a user on? Why does it matter? To separate the page as a concern, we have to consider how much control the current 'location' of a user has over our design, and whether that control is warranted. It's too easy let the Page micro-manage other concerns, which is unfortunate, because the Page is pretty bad at it — reduce Page responsibilities whenever you can.

Unseparated:

In this example, the about and contact pages have a special contact form that appears the footer. The design calls for some additional padding above the form. You might think to handle it this way:

 .about-page .footer,
 .contact-page .footer {
 padding-top: 30px;
 }
Separated:

...but really, it's the form, not the page, that makes the difference. Using the page class just because it's there might be convenient, but it couples two very different concerns badly. Instead, we can give the footer the ability to modify itself, using a smart name that tells us why the modification is necessary:

 .form-footer {
 padding-top: 30px;
 }

Component

A reusable, portable, piece of the interface (this one should look pretty familiar).

At the very least, you can give components a consistent interface in all three languages (by naming them), but you can go further by isolating their bits (CSS, JS, images, possibly markup as a template/partial) into a single directory. Give them subclasses for variations, instead of relying on the cascade or the DOM context. If you find that they interact with other components too much, give them a top-level object to talk to instead.

Each component is a concern, however minor. The key to durable components is separating them correctly from your other concerns.

Unseparated:

There are a few problems here. First, the component has mis-matching names throughout. Second, a supposedly self-contained component is scattered between several different, larger files. Third, the component itself reaches out to the Page.

 // show.html.erb
 <ul class="photos">
 <% @post.images.each do |img| %>
 <li><%= image_tag(img) %></li>
 <% end %>
 </ul>

 // application.js
 $.fn.carousel = function(methodName) {
 if ($(body).hasClass('gallery')) {
 addPagers(this);
 }
 ...
 };

 $('.photos').carousel();

 // application.css.scss
 .js .photos {...}
Separated:

This concern can be cleaned up with some renaming, some new files, and a small interface provided by a data-attribute. Now, it's much more neatly encapsulated.

 // _carousel.html.erb
 <ul class="carousel" data-pagers="true">
 <% images.each do |img| %>
 <li><%= image_tag(img) %></li>
 <% end %>
 </ul>

 // components/carousel/scripts.js
 $.fn.carousel = function(methodName) {
 var options = ({}, $(this).data());
 options.pagers && addPagers(this);
 ...
 };

 $('.photos').carousel();

 // components/carousel/styles.css.scss
 .carousel {...}

Content

When I say Content, I don't mean 'everything on the page' — there's a big distinction between blocks of big, readable, WYSIWYGable text and the other stuff. WYSWIGable areas are wildly unpredictable. They're constantly breaking out of their boxes, using incorrect heading tags, vomiting out iframes, and worse, they have tons of completely unclassed markup that you're suppose to anticipate and make pretty.

The traditional stylesheet wisdom is to style all your baseline elements first, and override those styles for other components, but this mixes the concerns (content, component) pretty badly. One way to detangle them is to apply Content styles/behaviors only in specific contexts:

Unseparated:

Setting the baseline styles up front means that later, we have to unset them. In this case, that means that every UL on the site is tied to this one — when this UL changes, they might all have to change with it.

 // show.html.erb:
 <%= @post.content %>

 // application.css.scss
 h2 {
 font-size: 20px;
 margin: 0 0 15px;
 }

 p {
 margin: 0.4em 0;
 }

 ul {
 list-style-type: disc;
 margin-left: 20px;
 }

 .sidebar p {
 margin: 0;
 }

 .sidebar ul {
 list-style-type: none;
 margin-left: 0;
 }
Separated:

Now that Content is treated like a separate concern, things clean up quite a bit. We can now safely work on styles inside .txt areas without worrying about how they affect other concerns in the interface.

 // show.html.erb:
 <div class="txt">
 <%= @post.content %>
 </div>

 // application.css.scss
 h2, 
 p, 
 ul {
 list-style-type: none;
 margin: 0;
 }

 .txt {
 h2 {
 font-size: 20px;
 margin: 0 0 15px;
 }

 p {
    margin: 0.4em 0;
   }

  ul {
    list-style-type: disc;
     margin-left: 20px;
   }
 }

Window

With responsive design, changes to the window become important information that informs both behavior and style. It's easy to start mixing in media queries and window.innerWidth checks to your code, but the further down this path you go, the more Window starts to feel like its own concern.

Unseparated:

This code doesn't look terrible, but it scales poorly. The window clearly has a special state (<320px), but there's no communication about what this state means, and the logic for it might need to be re-applied across many instances. The Gallery actually binds a window-size check to the window itself.

 @media screen and (max-width: 320px) {
 .content,
 .aside,
 .callouts .col,
 .nav {
 float: none;
 }
 }

 var Gallery = function() {
 $(window).resize(function() {
 if(this.enabled && window.innerWidth < 320) {
 this.disable();
 }
 }
 ...
 }
Separated:

The separated version moves responsibility to an 'appWindow' object that publishes events out to any listening components with some help from a super-dumb  pub/sub-type pattern.

 @media screen and (max-width: 320px) {
 .stack-at-phone {
 float: none;
 }
 }

 ...

 appWindow = {
 subscribers: [],
 prevWidth: $(window).width(),

 init: function() {
 $(window).resize(this.checkWidth.bind(this));
 this.checkWidth();
 }, 

 checkWidth: function() {
 var width = $(window).width();
 var sizingDown = width < 320 && t​his.prevWidth > 320;
 var sizingUp = width > 320 && t​his.prevWidth < 320;
 sizingUp && this.publish('phone');
 sizingDown && this.publish('unphone');
 this.prevWidth = width;
 },

 publish: function(eventName) {
    $.each(appWindow.subscribers, function() {
      this();
    });
  },

 subscribe: function(method) {
 this.subscribers.push(method);
 },
 
 ...
 };

 var Gallery = function() {
 appWindow.subscribe('phone', this.disable);
 appWindow.subscribe('unphone', this.enable);
 ...
 };


Tracking

If you're serious about tracking users' web use with GA (we are!), you'll know that tracking often puts its stubby fingers into the many pies of your other concerns. Good analytics folk want to track all kinds of interactions — page resizes, carousel navigation, scroll distance, you name it — and it can lead to a lot of disparate JS sprinkled around your application.

Because so many concerns need tracking, it can be hard to completely separate them, but you can at least encapsulate everything you can into a single object. Using a plugin like Trackiffer, or the data-track-event approach, you can even wire your markup straight to your tracking with a thin interface.

Unseparated:

Finding and binding markup like this is unmaintainable at a certain scale. Calling _gaq.push directly also gets tricky past a point — it hamstrings you when you want to add transformations or filtering to your tracking (for example, escaping characters).

 // analytics.js
 $('.callouts a').click(function() {
 _gaq.push(['_trackEvent', 'Callout', $(this).text()]);
 });

 // gallery.js
 gallery.$pager.click(function() {
 _gaq.push(['_trackEvent', 'Gallery', 'Page']);
 });
Separated:

Separation here is a matter of creating interfaces. First, between the markup and the tracking concern; allowing the markup to explicity talk to the tracking. Second, between everything else and the tracking concern — creating a helpful object that can parse tracking data, watch for clicks, and take over the responsibility of tracking from other modules.

 // show.html.erb
 <a href="#" data-track-event="Callout|Rebate">Rebate</a>

 // gallery.js
 analytics.watch(this.$pager, 'Gallery', 'Page');

 // analytics.js
 $('body').on('click', 'a[data-track-event]', analytics.track);

Closing

I hope these points and examples, if nothing else, affected how you think about concerns in the context of front-end development. If you have any specifics you'd like to discuss, please add them to the comments.

Further reading:

Doug Avery

,
Posted in Article Category: #Code
on