17. Ruby Core и Stdlib. Разни

17. Ruby Core и Stdlib. Разни

17. Ruby Core и Stdlib. Разни

6 януари 2014

Честита Нова година!

Днес

Дати за изпита

The Ten Commandments of Egoless Programming

1. Understand and accept that you will make mistakes.

The point is to find them early, before they make it into production. Fortunately, except for the few of us developing rocket guidance software at JPL, mistakes are rarely fatal in our industry, so we can, and should, learn, laugh, and move on.

2. You are not your code.

Remember that the entire point of a review is to find problems, and problems will be found. Don't take it personally when one is uncovered.

3. No matter how much "karate" you know, someone else will always know more.

Such an individual can teach you some new moves if you ask. Seek and accept input from others, especially when you think it's not needed.

4. Don't rewrite code without consultation.

There's a fine line between "fixing code" and "rewriting code." Know the difference, and pursue stylistic changes within the framework of a code review, not as a lone enforcer.

5. Treat people who know less than you with respect, deference, and patience.

Nontechnical people who deal with developers on a regular basis almost universally hold the opinion that we are prima donnas at best and crybabies at worst. Don't reinforce this stereotype with anger and impatience.

6. The only constant in the world is change.

Be open to it and accept it with a smile. Look at each change to your requirements, platform, or tool as a new challenge, not as some serious inconvenience to be fought.

7. The only true authority stems from knowledge, not from position.

Knowledge engenders authority, and authority engenders respect – so if you want respect in an egoless environment, cultivate knowledge.

8. Fight for what you believe, but gracefully accept defeat.

Understand that sometimes your ideas will be overruled. Even if you do turn out to be right, don't take revenge or say, "I told you so" more than a few times at most, and don't make your dearly departed idea a martyr or rallying cry.

9. Don't be "the guy in the room."

Don't be the guy coding in the dark office emerging only to buy cola. The guy in the room is out of touch, out of sight, and out of control and has no place in an open, collaborative environment.

10. Critique code instead of people – be kind to the coder, not to the code.

As much as possible, make all of your comments positive and oriented to improving the code. Relate comments to local standards, program specs, increased performance, etc.

Шарлатан

В дванадесетото предизвикателство искахме от вас да реализирате прост DSL:

Charlatan.trick do
  pick_from 1..10
  multiply_by 2
  multiply_by 5
  divide_by :your_number
  subtract 7
  you_should_get 3
end

Грешки и неподходящи решения

Грешка: дефиниране на методи в Object

Това е лоша идея, понеже "замърсява" всички обекти. В този пример, употребата на класови променливи също не е добра идея.

module Charlatan
  def Charlatan.trick(&block)
    block.call
  end
end

class Object
  def pick_from(range)
     @@random_number = Random.rand(range)
     @@current_number = @@random_number
  end

  # ...
end

Грешка: дефиниране на методи в Object

Ако случайно някой все още не знае, това е същата грешка като на предишния слайд:

module Charlatan
  def self.trick
    yield
  end
end

def pick_from(range)
  @range = range.to_a.zip range.to_a
end

def add(number)
  @range.each { |n| n[1] += (number != :your_number ? number : n[0]) }
end

# ...

Грешка: използване на глобален контекст

Да ползвате класов/модул контекст е същото като да ползвате глобални променливи и носи същите негативи. Най-малкото, кодът ви няма да е thread-safe.

module Charlatan
  def self.trick(&block)
    instance_eval &block
  end

  def self.pick_from(range)
    @result = range.to_a.dup
    @your_number = range.to_a.dup
  end

  # ...
end

Грешка: дефинирате помощни класове извън Charlatan

Това не е толкова фатално, но пак "замърсява" глобалното пространство с класове, които са всъщност вътрешни и специфични за вашата имплементаация:

module Charlatan
  class << self
    def trick(&block)
      trick_object = Trick.new
      trick_object.instance_eval(&block)
    end
  end
end

class Trick
  def initialize
    @numbers = {}
  end

  # ...
end

Грешка: не ползвате instance_eval

Признавам, това решение ме свари неподготвен:

module Charlatan
  def self.trick(&block_to_validate)
    block_source = extract_source block_to_validate
    trick_from_source_code(block_source).valid?
  end

  private

  def self.extract_source(block)
    file_path, start_line =  block.source_location
    source = File.open(file_path, "rb") { |file| file.read }
    source.lines.slice(start_line, source.lines.size).take_while { |line| line.match(/end/) == nil }
  end

  # ...
end

Грешка: не ползвате instance_eval (2)

Получило се е нещо като мини parser за Ruby код :)

module Charlatan
  def self.trick_from_source_code(block_source)
    operations = []
    range, expected_result = nil

    block_source.map  do |line|
      _, instruction_name, argument = line.match(/\s*(\S*)\s*(\S*)/).to_a

      case instruction_name
      when "pick_from"
        range = Range.new(*argument.split("..").map(&:to_i))
      when "you_should_get"
        argument = argument.start_with?(":") ? nil : argument.to_i
        expected_result = argument
      else
        argument = argument.start_with?(":") ? nil : argument.to_i
        operations << {available_operations[instruction_name.to_sym] => argument}
      end
    end
  end
end

under_score конвенция за имена на методи

Ако случайно се чудите как да приложите конвенцията в следния случай, то отговорът е, че методът трябва да се казва get_dsl, а не get_DSL:

class Charlatan
  def self.get_DSL
    CharlatanDSL.new
  end

  # ...
end

Нашето решение

Дефинираме отделен клас, инстанциите на който ще бъдат "контекст" на оценката на DSL-а:

module Charlatan
  class Evaluator
    # ...
  end

  def self.trick(&block)
    Evaluator.new.instance_eval &block
  end
end

Обърнете внимание на instance_eval-метода.

Evaluator (1)

Инстанционните методи на класа са нашите DSL "глаголи":

class Evaluator
  def initialize
    @operations_queue = []
  end

  def pick_from(range)
    @numbers = range
  end

  # ...
end

Evaluator (2)

Използваме малко метапрограмиране, за да намалим дублирането на логика:

class Evaluator
  operations = {
    add:         :'+',
    subtract:    :'-',
    multiply_by: :'*',
    divide_by:   :'/',
  }

  operations.each do |operation_name, operation|
    define_method operation_name do |argument|
      @operations_queue << [operation, argument]
    end
  end

  # ...
end

To metaprogram or not to metaprogram? (1)

class Charlatan
  def initialize
    @operations = []
  end

  def add(number)
    @operations << [:+, number]
  end

  def subtract(number)
    @operations << [:-, number]
  end

  def multiply_by(number)
    @operations << [:*, number]
  end

  def divide_by(number)
    @operations << [:/, number]
  end
end

To metaprogram or not to metaprogram? (2)

To metaprogram or not to metaprogram? (3)

От друга страна, това вече е твърде много дублиране:

Action = Struct.new(:type, :number)
class TrickSet
  def initialize()
    @actions = Array.new
  end
  def add(number)
    action = Action.new
    action.type = "add"
    action.number = number
    @actions.push(action)
  end
  def subtract(number)
    action = Action.new
    action.type = "subtract"
    action.number = number
    @actions.push(action)
  end
  def multiply_by(number)
    action = Action.new
    action.type = "multiply_by"
    action.number = number
    @actions.push(action)
  end
  def divide_by(number)
    action = Action.new
    action.type = "divide_by"
    action.number = number
    @actions.push(action)
  end
  # ...
end

To metaprogram or not to metaprogram? (4)

Evaluator (3)

Реалната проверка става в you_should_get, който прилага операциите, наредени на опашката, над всяко число:

class Evaluator
  # ...

  def you_should_get(argument)
    @numbers.all? do |number|
      expected = argument == :your_number ? number : argument
      expected == perform_operations_on(number)
    end
  end

  private

  def perform_operations_on(number)
    @operations_queue.reduce(number) do |current_result, (operation, argument)|
      argument = number if argument == :your_number
      current_result.public_send operation, argument
    end
  end
end

Въпроси дотук?

Ако имате въпроси по нещата, казани дотук, може да ги зададете сега.

String#%

String#%

пример с позиционни аргументи

Когато имаме повече от една стойност за интерполация, трябва да подадем списък:

'%02d' % 7                             # "07"
'%02d:%02d:%02d' % [19, 5, 2]          # "19:05:02"

name = 'Пешо'
'Здравей, <strong>%s</strong>!' % name # "Здравей, <strong>Пешо</strong>!"

String#%

пример с именувани аргументи

template = '
  <html>
  <head>
    <title>%{title}</title>
  </head>
  <body>
    %{content}
  </body>
  </html>
'

variables = {
  title:   'String Interpolation',
  content: 'Made easy by Kernel.sprintf.',
  foobar:  'Extra keys are not an issue.'
}

template % variables # "\n  <html>\n  <head>\n    <title>String Interpolation</title>\n  </head>\n  <body>\n    Made easy by Kernel.sprintf.\n  </body>\n  </html>\n"

Трета задача

Раждането на трета задача

Кратък преглед на нашето решение

Споменавал съм повечето неща в коментари към вашите решения, но въпреки това, ще разгледаме накратко нашето решение.

Тестове на трета задача

Искам да обърна внимание, че в хранилището с домашните има два теста за трета задача

  1. full_spec.rb
  2. spec.rb

Canvas#render_as

Нека си припомним Canvas#render_as:

module Graphics
  class Canvas
    def render_as(renderer)
      renderer.new(self).render
    end
  end
end

Тестове за Canvas#render_as

Забележете, че нито разчитат на, нито зависят от съществуването на renderer-и:

describe "Graphics" do
  describe "Canvas" do
    it "returns a rendering via render_as with external renderers" do
      null_renderer = Class.new do
        def initialize(canvas)
          @canvas = canvas
        end

        def render()
          "canvas: #{@canvas.width}x#{@canvas.height}"
        end
      end

      Graphics::Canvas.new(800, 600).render_as(null_renderer).should eq 'canvas: 800x600'
    end
  end
end

Тестове за Canvas#draw

Аналогично за Canvas#draw – тестът не зависи от shape:

module Graphics
  class Canvas
    def draw(shape)
      shape.rasterize_on(self)
    end
  end
end

describe "Graphics" do
  describe "Canvas" do
    it "allows drawing of shapes on itself" do
      shape = double
      shape.should_receive(:rasterize_on).with(canvas)
      canvas.draw(shape)
    end
  end
end

Тестове

Point#rasterize_on

Ето как изглеждат моите unit-тестове на Point#rasterize_on – не зависят от Canvas:

describe "Graphics" do
  context "shapes" do
    describe "Point" do
      context "rasterize_on" do
        it "draws the point on the canvas" do
          canvas = double
          canvas.should_receive(:set_pixel).with(3, 4)
          make_point(3, 4).rasterize_on(canvas)
        end
      end
    end
  end
end

Line#rasterize_on

Тестовете на Line#rasterize_on са по-различни, обаче, поради по-сложния алгоритъм на растеризация. Те зависят от рендерерите и от паното:

describe "Graphics" do
  context "shapes" do
    describe "Line" do
      context "rasterize_on" do
        it "draws simple horizontal lines" do
          canvas = make_canvas 8, 3
          canvas.draw make_line(make_point(3, 1), make_point(6, 1))

          check_rendering_of canvas, '
            --------
            ---@@@@-
            --------
          '
        end
      end
    end
  end
end

Проблеми със силно алгоритмични решения

BresenhamRasterization

Line#rasterize_on

Защо интеграционни тестове?

Въпроси дотук?

Ако имате въпроси по трета задача или друго от казаното дотук, сега може да ги зададете.

Ruby ядро и стандартна библиотека

Ruby core

Struct

Struct

пример от документацията

Виждате, че може да добавите и свои методи там

Customer = Struct.new(:name, :address) do
  def greeting
    "Hello #{name}!"
  end
end

dave = Customer.new('Dave', '123 Main')

dave.name     # "Dave"
dave.address  # "123 Main"
dave.greeting # "Hello Dave!"

Struct

Обектите от тип Struct приличат на колекции (хешове):

Customer = Struct.new(:name, :address, :zip)
john = Customer.new('John Doe', '123 Maple, Anytown NC', 12345)

john.name      # "John Doe"
john['name']   # "John Doe"
john[:name]    # "John Doe"
john[0]        # "John Doe"

john.length    # 3
john.each      # #<Enumerator: #<struct Customer name="John Doe", address="123 Maple, Anytown NC", zip=12345>:each>

Приложение на Struct

Удобно е да се ползва в тестове. За production код – по-рядко.

Comparable

Comparable

пример

class Money
  include Comparable

  attr :amount, :currency

  def initialize(amount, currency)
    @amount, @currency = amount, currency
  end

  def <=>(money)
    return unless currency == money.currency
    amount <=> money.amount
  end
end

Money.new(15, :BGN) < Money.new(30, :BGN) # true

Marshal

Marshal

пример

data = [42, :answer, {foo: 'bar'}]

serizlized = Marshal.dump data

serizlized                       # "\x04\b[\bi/:\vanswer{\x06:\bfooI\"\bbar\x06:\x06ET"
Marshal.load(serizlized) == data # true
Marshal.load(serizlized)         # [42, :answer, {:foo=>"bar"}]

Marshal

употреба и бележки

Следва продължение...

Ще разгледаме още класове от Ruby Core и Ruby Stdlib следващия път.

Въпроси