Clean Code
Before We Start
This material is created for Go Academy program.
This slide is not meant to be a complete summary of the Clean Code book by Robert C. Martin.
This slide will only cover parts of the Clean Code book that are relevant to the Go Academy curriculum.
Meaningful Names
Why Names Matter
Names are everywhere in our codes. We name our variables, functions, methods, packages, files, etc. Because we names things constantly in programming, we better do it well.
Show Intent (1)
Good names reveal intent.
d = Time.now.strftime('%Y/%m/%d')
Non-revealing name:
current_date = Time.now.strftime('%Y/%m/%d')
We should choose a name that specifies what is being measured and the unit of that measurement:
Show Intent (2)
Names that reveal intent make it easier to understand and change code.
def get_it(a, b)
if @cells[a]
@cells[a][b]
end
end
def cell_at(x, y)
if @cells[x]
@cells[x][y]
end
end
Compare these two codes:
Pronounceable and Searchable
For the same type of variable, use consistent vocabulary.
class DtaRcrd
def initialize(n, e)
@name = n
@email = e
end
end
Bad:
class User
def initialize(name, email)
@name = name
@email = email
end
end
Good:
Class Names and Method Names
Classes and objects should have noun or noun phrase name.
class Customer
end
class WikiPage
end
def initialize
end
def process_payment
end
Methods should have verb or verb phrase name.
Pick One Word per Concept
When we have decided what name we use for a concept, use it consistently.
For instance, if we decide that to retrieve a record from database we use "get", use it consistently. Don't mix it with other verbs such as "fetch" or "retrieve".
Functions and Methods
Small
Good functions and methods are small.
In Go-Jek, we have a coding guideline that states that we should not write a function or a method that has more than 15 lines.
Do One Thing
Good functions and methods do one thing only but they do it well.
def email_clients(clients)
clients.each do |client|
client_record = database.lookup(client)
email(client) if client_record.active?
end
end
Bad:
def email_active_clients(clients)
clients
.select(&method(:active_client?))
.each(&method(:email))
end
def active_client?(client)
client_record = database.lookup(client)
client_record.active?
end
Good:
Use Descriptive Names
Function and method names should be descriptive. A long but descriptive name is better than a short but enigmatic name.
def add_date(date, month)
end
Bad:
def add_month_to_date(date, month)
end
Good:
Prefer Fewer Arguments
Whenever possible, prefer functions and methods with fewer arguments.
def calculate_distance(source_point, destination_point)
end
Prefer:
def calculate_distance(x1, y1, x2, y2)
end
Than this:
Have No Side Effects
A function or method should not do anything other than what its name describe. State changing actions such as as writing files, storing to database, and changing a variable's value should never be done as a side effect. If we want our function or method to change a state, design it appropriately.
Don't Repeat Yourself (1)
Avoid duplicate code. Having duplicate code mean you have to change in multiple places when the logic of the code changes.
Rails' partials view is a good example of DRY principle in action.
Don't Repeat Yourself (2)
# new.html.erb
<h1>New Tag</h1>
<%= form_for(tag) do |f| %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<%= link_to 'Back', tags_path %>
Before:
# edit.html.erb
<h1>Editing Tag</h1>
<%= form_for(tag) do |f| %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
<%= link_to 'Show', @tag %> |
<%= link_to 'Back', tags_path %>
Don't Repeat Yourself (3)
# new.html.erb
<h1>New Tag</h1>
<%= render 'form', tag: @tag %>
<%= link_to 'Back', tags_path %>
After:
# _form.html.erb
<%= form_for(tag) do |f| %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
# new.html.erb
<h1>New Tag</h1>
# edit.html.erb
<h1>Editing Tag</h1>
<%= render 'form', tag: @tag %>
<%= link_to 'Show', @tag %> |
<%= link_to 'Back', tags_path %>
Objects and Data Structure
Data Abstraction
When designing classes (that will be instantiated as objects), we want to keep our variables private and provide abstraction for the outside world to interact with our objects.
public class Point {
public double x;
public double y;
}
Bad:
public interface Point {
public getX();
public getY();
void setCartesian(double x, double y);
}
Good:
Data/Object Anti-Simmetry
Objects hide their data behind abstraction and expose functions that operate on data.
Data structures expose their data and has no meaningful functions.
From this point of view, objects and data structures are an exact opposite.
The Law of Demeter (1)
A method f of a class C should only call the methods of these:
- C
- Objects created by f
- Objects passed as arguments to f
- Objects held in instance variables of C
The Law of Demeter (2)
product.provider.name
provider.address.city
company.building.city
Bad:
class Product < ActiveRecord::Base
belongs_to :provider
def provider_name
provider.name
end
end
class Provider < ActiveRecord::Base
has_many :products
end
product.provider_name
Good:
The Law of Demeter (3)
class Product < ActiveRecord::Base
belongs_to :provider
delegate :name, to: :provider, prefix: true
end
class Provider < ActiveRecord::Base
has_many :products
end
product.provider_name
Even better, delegate:
Data Transfer Object
The quintessential form of a data structure is a class with public variables and no functions. This is sometimes called a data transfer object, or DTO.
Active Records are special forms of DTOs. They are data structures with public (or bean-accessed) variables; but they typically have navigational methods like save and find. Typically these Active Records are direct translations from database tables, or other data sources.
We will get to know more on Active Records when we learn Ruby on Rails later on.
Unit Tests
The Three Laws of TDD
When we are writing codes in TDD style, there are three important rules:
1. You may not write production code until you have written a failing unit test.
2. You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
3. You may not write more production code than is sufficient to pass the currently failing test.
We will learn more about TDD in a separate session.
One Assert per Test (1)
Good tests only assert one thing per test.
require 'rspec'
describe 'Calculator' do
let(:calculator) { Calculator.new }
it 'performs addition, subtraction, multiplication and division' do
expect(calculator.calculate('1 + 2')).to eq(3)
expect(calculator.calculate('4 - 2')).to eq(2)
expect(calculator.calculate('2 * 3')).to eq(6)
expect(calculator.calculate('6 / 2')).to eq(3)
end
end
Bad:
One Assert per Test (2)
require 'rspec'
describe 'Calculator' do
let(:calculator) { Calculator.new }
it 'performs addition' do
expect(calculator.calculate('1 + 2')).to eq(3)
end
it 'performs subtraction' do
expect(calculator.calculate('4 - 2')).to eq(2)
end
it 'performs multiplication' do
expect(calculator.calculate('2 * 3')).to eq(6)
end
it 'performs division' do
expect(calculator.calculate('6 / 2')).to eq(3)
end
end
Good:
F.I.R.S.T (1)
F - Fast
Tests should run quickly because we want to run tests frequently.
I - Independent
Each test should be independent of each other. One test should not be a prerequisite for another test.
R - Repeatable
Tests should be repeatable in any environment (dev, staging, production) and return the same result.
F.I.R.S.T (2)
S - Self validating
Tests should have boolean value: pass or fail. We should be able to tell the result of tests (which ones pass and which ones fail) without lot of hassle.
T - Timely
Unit tests should be written in timely fashion: just before writing the code that will make them pass.
Classes
Classes Should be Small
As with functions and methods, classes should be small. But how small is small?
Single Responsibility Principle (1)
Continuing from the previous point, the ideal "small" size of a class is: only one responsibility for each class.
The Single Responsibility Principle states that a class or module should have one, and only one, reason to change.
Single Responsibility Principle (2)
class Reporter
def send_report
users = User.where("last_logged_in_at >= ?", 1.week.ago)
users.each do |user|
message = "Id: #{user.id}\n"
message += "Username: #{user.username}\n"
message += "Last Login: #{user.last_logged_in_at.strftime("%D")}\n"
message += "\n"
end
Mail.deliver do
from "jjbohn@gmail.com"
to "jill@example.com"
subject "Your report"
body message
end
end
end
Bad:
Single Responsibility Principle (3)
class ReportDataCsvCompiler
attr_accessor :data
def initialize(data)
self.data = Array(data)
end
def format
heading << body
end
private
def heading
data.first.keys.join(",") << "\n"
end
def body
data.each_with_object("") do |str, row|
str << row.values.join(",") << "\n"
end
end
end
Good - Part 1:
Single Responsibility Principle (4)
class User
scope :logged_in_this_week, ->{ where("last_logged_in_at >= ?", 1.week.ago) }
end
class UserWeeklyReport
def self.name
"Weekly User Report"
end
def self.formatter
ReportDataCsvCompiler
end
def self.data
UserWeeklyReportData
end
def to_file
File.write("/tmp/my_report")
end
end
Good - Part 2:
Single Responsibility Principle (5)
class ReportMailer
attr_accessor :report, :recipient
def intiialize(report:, recipient:)
self.report = report
self.recipient = recipient
end
def deliver!
mail = Mail.new do
from "jjbohn@gmail.com"
to recipient
subject report.name
end
mail.add_file(report.to_file)
mail.deliver!
end
end
Good - Part 3:
Single Responsibility Principle (6)
class UserWeeklyReportData
def to_data
User.logged_in_this_week.map do |user|
{
id: user.id,
username: user.username,
last_logged_in_at: user.last_logged_in_at.strftime("%D"),
}
end
end
end
# Then in you client code
ReportMailer.new(UserWeeklyReport, "jill@example.com")
Good - Part 4:
Important to note that the SRP does mean all classes should have only one function/method. SRP, as the name clearly describes, means all classes should have only one responsibility.
Clean Code
By qblfrb
Clean Code
- 281