luuchorocha
luuchorocha

Reputation: 99

How do I refactor code that uses `instance_exec` several times?

I'm working on a class that generates a PDF using Prawn gem. I have some similar methods. All of them start with the same line. Here is the code:

module PDFGenerator
  class MatchTeamInfo
    include Prawn::View

    def initialize(match)
      @match = match
      @output = Prawn::Document.new page_layout: :landscape
      defaults
      header
      footer
    end

    def render
      @output.render
    end

    def defaults
      @output.instance_exec do
        font_size 16
        text 'hola'
      end
    end

    def header
      @output.instance_exec do
        bounding_box [bounds.left, bounds.top], :width  => bounds.width do
          text "Fulbo", align: :center, size: 32
          stroke_horizontal_rule
          move_down(5)
        end
      end
    end

    def footer
      @output.instance_exec do
        bounding_box [bounds.left, bounds.bottom + 25], :width  => bounds.width do
          stroke_horizontal_rule
          move_down(5)
          text "Tu sitio favorito!", align: :center
        end
      end
    end
  end
end

Is there a way to avoid @output.instance_exec in every method and use something like blocks? I tried it, but I can't get it work. Can I do something like this?

def apply
  @output.instance_exec do
    yield
  end
end

How am I supposed to define the code blocks?

Upvotes: 1

Views: 74

Answers (2)

Stefan
Stefan

Reputation: 114138

You can define a method document that returns a Prawn::Document instance.

Prawn::View will then delegated the method calls to that document. Here's an example:

module PDFGenerator
  class MatchTeamInfo
    include Prawn::View

    def initialize(match)
      @match = match
      defaults
      header
      footer
    end

    def document
      @document ||= Prawn::Document.new page_layout: :landscape
    end

    def defaults
      font_size 16
      text 'hola'
    end

    def header
      bounding_box [bounds.left, bounds.top], :width  => bounds.width do
        text "Fulbo", align: :center, size: 32
        stroke_horizontal_rule
        move_down(5)
      end
    end

    def footer
      bounding_box [bounds.left, bounds.bottom + 25], :width  => bounds.width do
        stroke_horizontal_rule
        move_down(5)
        text "Tu sitio favorito!", align: :center
      end
    end
  end
end

Example usage:

pdf = PDFGenerator::MatchTeamInfo.new(nil)
pdf.save_as('team_info.pdf')

Output: (converted to PNG)

team_info.pdf

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 120990

First of all, you need to make all helper methods to return lambda instance:

def defaults
  lambda do
    font_size 16
    text 'hola'
  end
end

Now you might pass lambdas returned by your helpers to instance_exec. To acknowledge it about “this is code block rather than regular param,” lambda is to be prefixed with ampersand:

def apply
  #                     ⇓ NB! codeblock is passed!
  @output.instance_exec &defaults
end

If you want to pass a codeblock to apply, you should re-pass it to instance_exec. Unfortunately I know no way to re-pass it using yield keyword, but here is a trick: Proc.new called without parameters inside a method that was called with a codeblock given, is instantiated with this codeblock, so here you go:

def apply
  raise 'Codeblock expected' unless block_given?
  @output.instance_exec &Proc.new
end

Upvotes: 2

Related Questions