Reading
Read the following chapters from The Pragmatic Programmer, anniversary edition (in our library):
- Chapter 1, Topic 3: Software Entropy
- Chapter 2, Topic 9: The Evils of Duplication
- Chapter 2, Topic 10: Orthogonality
- Chapter 5, Topic 28: Decoupling (and the Law of Demeter)
Read the following chapters from Clean Code (in our library):
- Chapter 1: Clean Code
- Chapter 2: Meaningful Names
- Chapter 3: Functions
- Chapter 4: Comments
- Chapter 5: Formatting
- Chapter 8: Boundaries
- Chapter 10: Classes
- Chapter 12: Emergence
- Chapter 17: Smells and Heuristics
Also read:
- How to write modular code
- Keep It DRY, Shy, and Tell the Other Guy Show archive.org snapshot
- GRASP (object-oriented design) Show archive.org snapshot
- Single Responsibility Principle Show archive.org snapshot
- Tell, Don't Ask Show archive.org snapshot
- Best practices for writing code comments Show archive.org snapshot
Read the following chapters from our book Growing Rails Application in Practice:
-
- Dealing with fat models
-
- Extracting service objects
Discuss with your mentor what you took away from each topic.
Rules of thumb
Apart from the concepts mentioned above, there are a many more rules that are good to follow most of the time. For example:
- Embrace Locality: Avoid cluttering one concept across the entire code base. It should be encapsulated in single files or folders.
-
Be easy to call: A service should always try to have a simple public interface, even if that causes additional complexity within the class. For example:
- Interacting with the model should be easy for controllers and views
- Interacting with helpers and routes should be easy for views
- Avoid long instruction manuals: If you need to write down an A4-letter for your colleagues on how to use your service, there might be a way to refactor it to a simpler public interface. A post-it sized note should be enough, if anything!
Exercise
We're building an e-commerce app where users can create and view invoices.
This is our current model:
class Invoice < ApplicationRecord
has_many :items
validates_presence_of :recipient_address, :number
end
class Invoice::Item < ApplicationRecord
belongs_to :invoice
belongs_to :product
validates_numericality_of :units
end
class Product < ApplicationRecord
validates_presence_of :description
validates_numericality_of :unit_price
end
This is a view that shows an invoice:
%h1
Invoice
= @invoice.number
%h2 Recipient
= @invoice.recipient_address
%h2 Items
%table
%tr
%th Description
%th Quantity
%th Item total
- @invoice.items.each do |item|
%td= item.product.description
%td= item.units
%td= item.units * item.product.unit_price
%tr
%th
Invoice total
%td(colspan=3)
= @invoice.items.sum { |item| item.units * item.product.unit_price } * 1.19
How would you judge the quality of this code?
Try to apply what you learned with a refactoring of the model and the view. What are the advantages of your solution?
Exercise: MovieSearch vs. Authorization
Let's say there is the a MovieSearch
class in your project with the following public API:
class MovieSearch
def initialize(query)
@query = query
end
def results
Movie.where('title LIKE ?', @query)
end
end
search = MovieSearch.new('Interstellar')
search.results.each do |movie|
puts movie.title
end
Now the current user should not be allowed to search all movies, but only a subset based on their role and the movie's state. A naive inline authorization could look like this:
class MovieSearch
def initialize(query, current_user)
@query = query
@current_user = current_user
end
def results
scope = Movie.where('title LIKE ?', @query)
unless current_user.moderator?
scope = scope.where('state = "approved" OR user_id = ?', @current_user.id)
end
scope
end
end
Try to come up with an alternative implementation where the two concepts (movie search and authorization) are less coupled.