Lessons Learned from Writebook
37 Signals (37s) have generously released the source code to Writebook, their latest offering under the Once suite of products. There is no Github repository, but the source code is readily available. It is accepted with thanks and gratitude.
I’m going to post snippets of the code (without permission) sharing my “lessons learned”. I doubt anyone would object, seeing that 37s does similar with closed source code. But you never know, and if any do come, for whatever reason, I’d remove the post.
Immediately Adopt What is Useful
If you see something that is worthy of copying - then do so, immediately.
How to read their code base?
Here’s where to start:
- Gemfile
- Routes: what is the root_path?
- Then go straight to the tests.
Gemfile
- Minimise dependencies: that is an extra layer of difficulty.
No factory-bot
, and no rspec
The greatest regret of my life? Falling into the factory-bot
paradigm. Simple and clean fixtures are all that are required. I never understood detractors cavilling at “fixtures” being brittle. How are they any more brittle than the factory methods in factory-bot
?
title
and open graphs
Every page has a title. Don’t forget it!
They use open graphs. I didn’t even know what this was, but as soon as I saw it, I (basically) cut and pasted, and added it in my repositories:
<!-- application.html.erb -->
<head>
...
<%= content_for(:open_graph) %>
...
</head>
And then in the relevant page:
<% content_for :open_graph do %>
<%= tag.meta property: "og:title", content: "Project: #{quote.id}" %>
<%= tag.meta property: "og:description", content: truncate("#{quote.id} - #{quote.project_name} - #{quote.aasm_state} - #{quote.created_at}", :length => 80) %>
<!-- fix the image to a client specific logo -->
<%= tag.meta property: "og:image", content: asset_url("tek1.svg") %>
<%= tag.meta property: "og:image:url", content: asset_url("tek1.svg") %>
<%= tag.meta property: "og:image:secure_url", content: asset_url("tek1.svg") %>
<%= tag.meta property: "og:image:type", content: "image/svg+xml" %>
<%= tag.meta property: "og:image:width", content: "396" %>
<%= tag.meta property: "og:image:height", content: "182" %>
<%= tag.meta property: "og:image:alt", content: "firm logo" %>
<%= tag.meta property: "og:url", content: quote_url(quote) %>
<%= tag.meta property: "twitter:title", content: "Project: #{quote.id}" %>
<%= tag.meta property: "twitter:description", content: truncate("#{quote.id} - #{quote.project_name}", :length => 80) %>
<%= tag.meta property: "twitter:image", content: asset_url("tek1.svg") %>
<%= tag.meta property: "twitter:card", content: "summary_large_image" %>
<% end %>
For some reason, certain applications do not display svg
as well as they do png
files.
So I have immediately incorporated these lessons.
Implicit Concepts and knowledge
They seem to rely heavily on industry-based concepts. For example, in the publishing industry, “leaf”, “leaves” and “press” are terms of art. 37 Signal relies on these concepts when naming models. This means, once you understand those concept, it’s much easier to understand the code-base.
I do similar with my code-base. For example, if you know anything about rock bands, all I need to mention is: “Queen”, “albums” and “songs”, and “News of the World” or “Bohemian Rhapsody” and you will know exactly what I’m talking about. What if I mentioned freddie
. You simply “know”: (i) what band he is in, and (ii) possibly what songs he has written, as well as his albums. I don’t have to explain anything new to you.
This aids immensely in writing and using fixtures.
New Methods
This is what 37 Signals does:
<!-- show.html.erb -->
<% content_for :header do %>
<%= render "leaves/header", book: @book, leaf: @leaf %>
<% end %>
I had typically done the following:
<!-- show.html.erb -->
<%= render "leaves/header", book: @book, leaf: @leaf %>
<!-- leaves/header.html.erb -->
<% content_for :header do %>
<%= render "leaves/header", book: @book, leaf: @leaf %>
<% end %>
In other words, I would render the partial, and keep content_for
within the partial rather than outside of it.
Having looked at someone else’s source code, I think I prefer it such that the partial does not have the content_for
method within it, because I want to see it in the show.html.erb
page.
Psuedo Currying of Code
Direct from the code base:
### first_run.rb
User.create!(user_params.merge(role: :administrator)).tap do |user|
DemoContent.create_manual(user)
end
But this is how I would typically do it:
user = User.create!(user_params.merge(role: :administrator))
DemoContent.create_manual(user)
It’s barbaric, but it still works: it’s not immediately obvious the two lines of code are “curried” (i.e. directly connected - with the output of the first is the input to the second).
Can you improve upon the expressiveness of 37s code?
# some other options
DemoContent.create_admin_user(user_params).then do |user|
create_manual(user) # I don't want to call DemoContent here.
end
# but why should you reach for DemoContent, when you want to create an admin user?
# psuedo code - does this work?
User.create_admin(user_params)
|> DemoContent.create_manual
# or this?
DemoContent.create_user(:admin, user_params).then do |user| # mixin in create_user into demo content?
create_manual(user)
end
# or using the 'then' keyword?
DemoContent.create_admin(user_params).then do |user|
create_manual(user)
end
User.create_admin(user_params).then do |user|
DemoContent.create_manual(user)
end
How should I choose? Choose what tastes right to you.
The use of helper methods and helper classes
The following is the first_run controller. What does it do?
- This is the first page you see when you deploy the app.
- The entire concept is self-explanatory, but there is no “first_runs” database table. All this controller does is create the first user, and creates the first write_book with that user as the admin.
- Also note - that if an bad actor wanted to take control and hoist themselves as admin before you do - that is possibile. 37s have rationalised that it is not worth securing everything, and the chances of that happening are so remote, it is not worth the extra overhead to manage.
class FirstRunsController < ApplicationController
allow_unauthenticated_access
before_action :prevent_running_after_setup
def show
@user = User.new
end
def create
user = FirstRun.create!(user_params)
start_new_session_for user
redirect_to root_url
end
private
def prevent_running_after_setup
redirect_to root_url if User.any?
end
def user_params
params.require(:user).permit(:name, :email_address, :password)
end
end
# first_run.rb
class FirstRun
ACCOUNT_NAME = "Writebook"
def self.create!(user_params)
account = Account.create!(name: ACCOUNT_NAME)
User.create!(user_params.merge(role: :administrator)).tap do |user|
DemoContent.create_manual(user)
end
end
end
If you wanted to be garrulous, you could write the above like this:
class FirstRunsController < ApplicationController
# .... etc.
def create
## the following 3 lines are extracted into first_runs
account = Account.create!(name: ACCOUNT_NAME)
user = User.create!(user_params.merge(role: :administrator)).tap do |user|
DemoContent.create_manual(user)
end
start_new_session_for user
redirect_to root_url
end
end
37s seem to want to simplify concepts at a higher level, and put everything in organised drawers.
I think this works well. Immensely well, especially when you have a large code-base.
Nesting Controllers
Suppose you have:
- pet owners, and
- pets
# pet_ower.rb
has_many :pets
# pet.rb
belongs_to :owner
Ordinarily, where would you put the pets_controller.rb
?
app/controllers/pets_controller.rb # dump it here?
No! Don’t do that. If you nest it, then you automatically know, by convention that a pet is defined by its owner.
37s nests it:
app/controllers/owners/pets_controller.rb
Does it make more sense to you?
# routes.rb
resources :books, except: %i[ index show ] do
resource :publication, controller: "books/publications", only: %i[ show edit update ]
resource :bookmark, controller: "books/bookmarks", only: :show
end
Notice how publications
and bookmarks
are nested in a particular folder structure? Moreover, with zeitwerk, they will be scoped under the Books
‘namespace’.
class Books::BookmarksController < ApplicationController
allow_unauthenticated_access
include BookScoped
def show
@leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
end
private
def last_read_leaf_id
cookies["reading_progress_#{@book.id}"]
end
end
Heavy use of Concerns
(a) Common Before Actions
How is the @book
instance variable set?
# /controllers/books/bookmarks_controller.rb
class Books::BookmarksController < ApplicationController
# etc ...
include BookScoped
def show
# how is @book set?
@leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
end
# etc.
end
Look into the BookScoped
concern:
module BookScoped extend ActiveSupport::Concern
included do
before_action :set_book
end
private
def set_book
@book = Book.accessable_or_published.find(params[:book_id])
end
end
How would I be doing it?
class Books::BookmarksController < ApplicationController
# etc ...
before_action :set_book
def show
# how is @book set?
@leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
end
private
def set_book
@book = Book.accessable_or_published.find(params[:book_id])
end
end
They are both doing the same thing, but one places common code away in a drawer - in a concern.
I might try out this technique and see if I like it.
… to be continued…