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.
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.
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.
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.
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.
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.
Knowledge engenders authority, and authority engenders respect – so if you want respect in an egoless environment, cultivate knowledge.
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.
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.
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
Това е лоша идея, понеже "замърсява" всички обекти. В този пример, употребата на класови променливи също не е добра идея.
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
Ако случайно някой все още не знае, това е същата грешка като на предишния слайд:
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
Това не е толкова фатално, но пак "замърсява" глобалното пространство с класове, които са всъщност вътрешни и специфични за вашата имплементаация:
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
Признавам, това решение ме свари неподготвен:
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
Получило се е нещо като мини 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
Ако случайно се чудите как да приложите конвенцията в следния случай, то отговорът е, че методът трябва да се казва 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
-метода.
Инстанционните методи на класа са нашите DSL "глаголи":
class Evaluator
def initialize
@operations_queue = []
end
def pick_from(range)
@numbers = range
end
# ...
end
Използваме малко метапрограмиране, за да намалим дублирането на логика:
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
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
От друга страна, това вече е твърде много дублиране:
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
Реалната проверка става в 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
е много удобен за интерполация и форматиране на определени низове
Kernel.sprintf
%
може да се ползва и без точка, като други методи, наподобяващи операториКогато имаме повече от една стойност за интерполация, трябва да подадем списък:
'%02d' % 7 # "07"
'%02d:%02d:%02d' % [19, 5, 2] # "19:05:02"
name = 'Пешо'
'Здравей, <strong>%s</strong>!' % name # "Здравей, <strong>Пешо</strong>!"
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"
Споменавал съм повечето неща в коментари към вашите решения, но въпреки това, ще разгледаме накратко нашето решение.
Искам да обърна внимание, че в хранилището с домашните има два теста за трета задача
spec.rb
full_spec.rb
full_spec.rb
е по-тясно свързан с особеностите на моя дизайн и е продукт от TDD-то ми
spec.rb
е малко по-близо до понятието "интеграционен тест"
full_spec.rb
Нека си припомним Canvas#render_as
:
module Graphics
class Canvas
def render_as(renderer)
renderer.new(self).render
end
end
end
Забележете, че нито разчитат на, нито зависят от съществуването на 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
– тестът не зависи от 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
, Line
и Rectangle
spec.rb
е по-близо до интеграционен тест, за да ви позволи свобода в дизайна
Canvas#draw(shape)
+ Canvas#render_as(Graphics::Renderers::Ascii)
Ето как изглеждат моите 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
са по-различни, обаче, поради по-сложния алгоритъм на растеризация. Те зависят от рендерерите и от паното:
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
private
методи в Line
private
методи
BresenhamRasterization
private
методиBresenhamRasterization
, това не е съвсем правилно
Line#rasterize_on
ползва публичния интерфейс на BresenhamRasterization
BresenhamRasterization
и неговите методиАко имате въпроси по трета задача или друго от казаното дотук, сега може да ги зададете.
core
; сега ще видим още няколко интересни класа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
приличат на колекции (хешове):
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>
Удобно е да се ползва в тестове. За production код – по-рядко.
<=>
и правите include Comparable
<
, <=
, ==
, >=
, >
и between?
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.load
и Marshal.dump
marshal_dump
и marshal_load
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
може да се ползва YAML
, от стандартната библиотека
Ще разгледаме още класове от Ruby Core и Ruby Stdlib следващия път.