Build Robust & Production Quality Applications - Lesson 8: Message Expectations

Messgage Expectations

The best resource for docs on message expectations. Message expectations is what is referred to mocking.

Build Robust & Production Quality Applications - Lesson 8: Beyond MVC - Object Composition, Object Oriented Design, & YAGNI

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).

Object Composition

We can take some of the responsibility from those objects however, this does come at a cost.

In-direction

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.

YAGNI (You Aren't Going to Need It)

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.

Build Robust & Production Quality Applications - Lesson 8: Beyond MVC - Service Objects

Service Objects

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

Create the Service Policy called CreditDeduction

#app/services/credit_deduction.rb
Class CreditDeduction


end

Extract business process related Code

#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

Update the TodoController

#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

Move instatiating a new Credit Object to your Service Policy

#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

Build Robust & Production Quality Applications - Lesson 8: Beyond MVC - Domain Objects

Domain Objects

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

Build Robust & Production Quality Applications - Lesson 8: Beyond MVC - Policy Objects

Policy Objects

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