Permalink
Browse files

refactor helpers into decorators in README

  • Loading branch information...
moonmaster9000 committed Aug 25, 2012
1 parent 1f4c006 commit 9502d9b7ce2f15dbe5367c8fe377a8c7e5e7e388
Showing with 94 additions and 80 deletions.
  1. +94 −80 readme.markdown
View
@@ -23,126 +23,152 @@ $ rails g frill Timestamp --test-framework=rspec
create spec/frills/timestamp_frill_spec.rb
```
-## Usage
+## Refactoring timestamp helpers with decorators
-(For the purposes of this tutorial, I'm going to assume you're using
-`frill` inside a Rails app. Checkout the `Usage outside Rails` section
-below if you're not using Rails.)
+Your product manager writes the following story for you:
-Imagine you're creating a web application that includes both a
-JSON API and an HTML frontend, and you've decided to always present a
-timestamp as YEAR/MONTH/DAY. Furthermore, when presented in HTML, you
-always want your timestamps wrapped in `<b>` tags.
+```cucumber
+Feature: Consistent Timestamp Presentation
+ As a user
+ I want "created at" timestamps presented in a uniform way on the site
+ So that I can easily discover the age of content on the site
-This is a perfect fit for the GoF decorator pattern. Start by generating a `Timestamp` frill:
+ Scenario: Presenting timestamps
+ When I navigate to a page that displays a created_at timestamp
+ Then I should see that timestamp marked up as bold and formatted as follows: YYYY/MM/DD
+```
-```sh
-$ rails g frill Timestamp --test-framework=rspec
- create app/frills/timestamp_frill.rb
- invoke rspec
- create spec/frills/timestamp_frill_spec.rb
+You see this and roll your eyes. You're thinking about all of the places that you show `created_at`
+timestamps on the site. Regardless you roll up your sleeves and start by writing the following helper and partial:
+
+```ruby
+module ApplicationHelper
+ def format_timestamp(t)
+ render partial: "shared/timestamp", locals: { time: t.strftime "%Y/%m/%d" }
+ end
+end
```
-You can safely leave off the `--test-framework=rspec` portion if you've configured rspec as your default framework (or
-if you're not using rspec at all).
+```erb
+<b><%=time%></b>
+```
-Now open up `app/frills/timestamp_frill.rb` and format those timestamps:
+You then begin the tedious task of tracking down all of the places you render timestamps on the site and wrapping them with `format_timestamp` helper calls:
+
+```erb
+...
+Written on <%=format_timestamp @article.created_at %>
+```
+
+You hate this approach.
+
+1. It's tedious
+1. It's procedural
+1. Developers have to remember to manually wrap timestamps with your `format_timestamp` helper. Developers suck at remembering things like that. FACEPALM
+
+After you deliver the story, your product owner says "Great! But what about the format of timestamps in the JSON api? Here's another story."
+
+```cucumber
+Feature: Consistent Timestamp Presentation in the API
+ As an API consumer
+ I want "created at" timestamps presented in the API in uniform way
+ So that I can easily discover the age of data I consume
+
+ Scenario: Presenting timestamps
+ When I retrieve content with a "created_at" timestamp via the JSON API
+ Then I should see that timestamp formatted as follows: YYYY/MM/DD
+```
+
+You attempt to salvage the helper, updating it with concerns for the JSON format:
+
+```ruby
+module ApplicationHelper
+ def format_timestamp(t)
+ time = t.strftime "%Y/%m/%d"
+
+ if request.format.html?
+ render partial: "shared/timestamp", locals: { time: time }
+ elsif request.format.json?
+ time
+ end
+ end
+end
+```
+
+And now you begin the tedious track of updating all of the JSON views with the helper:
+
+```ruby
+json.created_at format_timestamp(@article.created_at)
+```
+
+At this point, you're banging your head against a table.
+
+### Enter Frill
+
+Let's refactor this using the decorator pattern. First, revert all of your changes. Next, add the `frill` gem to your Gemfile, run `bundle`, then generate a frill: `rails g frill TimestampFrill`:
```ruby
module TimestampFrill
include Frill
def self.frill? object, context
- object.respond_to?(:created_at) && object.respond_to?(:updated_at)
+ object.respond_to?(:created_at)
end
def created_at
- format_time super
- end
-
- def updated_at
- format_time super
- end
-
- private
-
- def format_time(t)
- t.strftime "%Y/%m/%d"
+ super.strftime "%Y/%m/%d"
end
end
```
-The first method `self.frill?` tells `Frill` what kind of objects this
-decorator is applicable to. In our case, it's any object that have timestamps.
+The `frill?` method tells `Frill` when to extend an object with this module. Then we redefine the `created_at` method,
+calling super and then formatting the date returned with `strftime`.
-Next, let's create an `HtmlTimestampFrill` module:
+Simple enough.
-```sh
-$ rails g frill HtmlTimestamp --test-framework=rspec
- create app/frills/html_timestamp_frill.rb
- invoke rspec
- create spec/frills/html_timestamp_frill_spec.rb
-```
+Next, generate another frill for presenting timestamps via HTML (`rails g frill HtmlTimestampFrill`):
```ruby
module HtmlTimestampFrill
include Frill
after TimestampFrill
def self.frill? object, context
- object.respond_to?(:created_at) &&
- object.respond_to?(:updated_at) &&
- context.request.format.html?
+ object.respond_to?(:created_at) && context.request.format.html?
end
def created_at
- format_time_for_html super
- end
-
- def updated_at
- format_time_for_html super
- end
-
- private
- def format_time_for_html t
- h.content_tag :b, t
+ h.render partial: "shared/timestamp", locals: { time: super }
end
end
```
-Two things to note: the `HtmlTimestampFrill` is only applicable to
-objects that have timestamps _when presented in "html"_. Also, we
-tell `Frill` to decorate `after` `TimestampFrill` is applied (so that
-`super` in `created_at` returns our `TimestampFrill` response).
+There's two important things to note:
-Note that you can also specify decoration dependencies with `before` instead of `after`.
+1. This frill comes after `TimestampFrill`. That tells `Frill` that it should only attempt to extend an object with this module after attempting to extend it with `TimestampFrill`.
+1. The `frill?` method only returns true if it's an HTML request, meaning this frill won't be extended onto objects for your JSON api.
-Next, in our controller, we need to decorate our objects with frills:
+Lastly, opt objects into frilling inside your controllers:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
respond_to :json, :html
def show
@article = frill Article.find(params[:id])
respond_with @article
end
-end
```
-Notice that we've wrapped our article in a `frill`.
-
-In your html view, you simply call `@article.created_at`:
+And that's it. You don't have to update any of your views. Why? When you call the `frill` method inside your controller and pass it an object (or a collection of objects),
+frill will attempt to extend the object with any applicable frills (i.e., frills that return `true` for the `frill?` method when passed the object and the request context).
-```erb
-<%= @article.created_at %>
-```
+That way, you can simple render your `created_at` attributes without any helpers, and they will automatically present themselves appropriately for their context (e.g., HTML v. JSON requests).
-The same goes for your JSON view.
### 'frill' decorates individual objects _and_ collections
-The `frill` helper will decorate both collections and associations. You can use it both within your controller
+As I've hinted at, the `frill` helper will decorate both single objects and collections of objects. You can use it both within your controller
and within your views.
For example, inside a controller:
@@ -169,7 +195,7 @@ Or, in a view:
There are really just two integrations in a Rails app: the `frill`
method inside of your controller, plus the ability to call
-`ActionView::Helper` methods inside of your module methods.
+helper methods inside of your module methods.
To kickoff the decoration of an object outside of a Rails application,
simply call `Frill.decorate`:
@@ -182,15 +208,3 @@ Frill.decorate my_object, my_context
* Ben Moss
* Nicholas Greenfield
-
-## License
-
-(The MIT License)
-
-Copyright © 2012 Matt Parker
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 comments on commit 9502d9b

Please sign in to comment.