Build Robust & Production Quality Applications - Lesson 8: Message Expectations
15 Jul 2015Messgage Expectations
The best resource for docs on message expectations. Message expectations is what is referred to mocking.
The best resource for docs on message expectations. Message expectations is what is referred to mocking.
Rails provides an MVC framework - but there is a point where the complexity of your application grows beyong this structure. Typically, when this first happens, devs move the logic from their controllers, to the models (skinny contollers/fat models) - but often we find that we are pushing too much logic into the models, making them bloated. Often times models at the center of your application, that are so large (in complexity and touches so many other objects) that people are afraid to touch it (becasue something bad will happen if you mess up) is called God Objects (God Models).
We can take some of the responsibility from those objects however, this does come at a cost.
When you extract logic to various objects within a process, it can be difficult to chase down what is happening as the process is not all in one view. Well defined names and interfaces.
However - there are some ways to abuse object composition.
is over-extracting or abstracting to a layer that is not useful at the moment, in hopes that it will be in the future, but you prolly won't. It is better to wait as you will never know less about the future than you do now.
Just like we can extract domain logic into domain objects, we can extract business level logic into service objects. Currently, the below process is modeling the Credit Deduction Business Process. Let's extract that into a Service Object called CreditDeduction.
#apps/controllers/todos_controller.rb
def create
@todo = Todo.new(params[:todo)]
credit = Credit.new(current_user)
if @todo.save_with_tags
if UserLevelPoicy.new(ccurent_user).premium?
credit = credit - 1
else
credit = credit - 2
end
credit.save
if credit.depleted? < 0
AppMailer.notify.insufficient_funds
elsif credit.low_balance?
AppMailer.notify_low_balance
end
redirect_to root_path
else
render :new
end
#app/services/credit_deduction.rb
Class CreditDeduction
end
#app/services/credit_deduction.rb
Class CreditDeduction
attr_accessor :user
def initialize(user)
@user = user
end
def deduct_credit
if UserLevelPoicy.new(ccurent_user).premium?
credit = credit - 1
else
credit = credit - 2
end
credit.save
if credit.depleted? < 0
AppMailer.notify.insufficient_funds
elsif credit.low_balance?
AppMailer.notify_low_balance
end
end
end
#apps/controllers/todos_controller
def create
@todo = Todo.new(params[:todo)]
credit = Credit.new(current_user)
if @todo.save_with_tags
CreditDeduction.new(credit, current_user).deduct_credit
redirect_to root_path
else
render :new
end
end
end
#app/services/credit_deduction.rb
Class CreditDeduction
attr_accessor :credit, :user
def initialize(credit, user)
@credit = Credit.new(user)
@user = user
end
def deduct_credit
if UserLevelPoicy.new(ccurent_user).premium?
credit = credit - 1
else
credit = credit - 2
end
credit.save
if credit.depleted? < 0
AppMailer.notify.insufficient_funds
elsif credit.low_balance?
AppMailer.notify_low_balance
end
end
end
#apps/controllers/todos_controller
def create
@todo = Todo.new(params[:todo)]
if @todo.save_with_tags
CreditDeduction.new(current_user).deduct_credit
redirect_to root_path
else
render :new
end
end
end
Domain Objects are objects that are within the domain object model. Not all of them inherit from active record though, often times they do not always map to database tables.
For important attributes, it may be worth it to create a seperate domain object rather than calling an object to access that attribute.
#apps/controllers/todos_controller.rb
def create
@todo = Todo.new(params[:todo)]
if @todo.save_with_tags
user.created_at < Date.new(2013, 1, 1) || user.plan.premium?
new_credit_balance = current_user.current_credit_balance - 1
else
new_credit_balance = current_user.current_credit_balance - 2
end
current_user.current_credit_balance = new_credit_balance
current_user.save
if new_credit_balance < 0
AppMailer.notify.insufficient_funds
elsif current_user.current_credit_balance < 10
AppMailer.notify_low_balance
end
redirect_to root_path
else
render :new
end
Let's create a domain called Credit:
#app/models/credit.rb
class Credit
attr_accessor :credit_balance, :user
def initialize(user)
@credit_balance = user.current_credit_balance
@user = user
end
def -(number)
credit_balance = credit_balance - number
end
def save
user.current_credit_balance = credit_balance
user.save
end
def depleted?
credit_balance < 0
end
def low_balance?
credit_balance < 10
end
end
Now let's update the Todos Controller:
#apps/controllers/todos_controller.rb
def create
@todo = Todo.new(params[:todo)]
credit = Credit.new(current_user)
if @todo.save_with_tags
if UserLevelPoicy.new(ccurent_user).premium?
credit = credit - 1
else
credit = credit - 2
end
credit.save
if credit.depleted? < 0
AppMailer.notify.insufficient_funds
elsif credit.low_balance?
AppMailer.notify_low_balance
end
redirect_to root_path
else
render :new
end
Do you have complicated code in your logic - cure that baby with Policy Objects! Policy Objects can be used to represent the state of an object:
#apps/controllers/todos_controller.rb
def create
@todo = Todo.new(params[:todo)]
if @todo.save_with_tags
user.created_at < Date.new(2013, 1, 1) || user.plan.premium?
new_credit_balance = current_user.current_credit_balance - 1
else
new_credit_balance = current_user.current_credit_balance - 2
end
#app/models/user_level_policy
class UserLevelPolicy
attr_reader :user
def initialize(user)
@user = user
end
def premuim?
user.created_at < Date.new(2013, 1, 1) || user.plan.premium?
end
def silver?
end
def bronze?
end