Oct 26 2009

Birthday Greetings Kata in Ruby

Category: agile,ruby,tutorialgiordano scalzo @ 9:30 pm

Lately the pratice of Kata seems spreading out very quickly, thanks to work of well known Software Craftsmen as Corey Haynes or the Clean Code evangelist Uncle Bob.

It all started waiting for a kid’s karate lesson, and now it’s a well know practice to become a better developer.

Basicly a code kata is a small problem to be resolved without any pressure and requests, but to play with different techniques or programming language.

Another interesting point of view considers a code kata as a training for muscles of memory, focusing on repetitions, memorizations of decisions and keyboard shortcuts; the point is: if I get trained to do design decisions at subconscious level, when I’ll met similar problems I’ll be very productive, doing the right decision without any logical think.
Amazing, isn’t it?

The main argument of last Milan Xpug meeting was the Birthday Greetings Kata, a workshop Matteo Vaccari will submit to next Xp Days Benelux 2009.
Unfortunately I couldn’t be present, but I implemented the kata on my own, using Ruby instead of Java.

I enjoyed it very much, and I start thinking to try it still a couple of times and then screencasting it: I’m far behind this level, but review my actions can help me get better.

$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
require 'rumbster'
require 'message_observers'
require 'net/smtp'
require 'gserver'
require 'birthday_service'
require 'employee_repository'

describe "Greetings Service" do
	NON_STANDARD_PORT = 10015
	def send_message(to, message)
    		Net::SMTP.start('localhost', NON_STANDARD_PORT) do |smtp|
      			smtp.send_message message, 'your@mail.address', to
    		end
	end
	before :each do
	 	@rumbster =  Rumbster.new(NON_STANDARD_PORT)
   		@message_observer = MailMessageObserver.new
		@rumbster.add_observer @message_observer
		@rumbster.start

		@birthdayService = BirthdayService.new("localhost", NON_STANDARD_PORT);
	end
	after :each do
		@rumbster.stop
	end
	context "with a file with one person born today" do
		before :each do
			@birthdayService.send_greetings EmployeeRepository.new("employee_data.txt"), "2008/10/08"
		end

		it "should send one email" do
			@message_observer.messages.size.should == 1
		end

		it "should send correct message" do
			@message_observer.messages.first.subject.should == "Happy Birthday!"
			@message_observer.messages.first.body.chomp.should == "Happy Birthday, dear John!"
			@message_observer.messages.first.to.should == ["john.doe@foobar.com"]
		end
	end

end
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
require "employee_repository"

describe "EmployeeRepository" do
	it "should read Employees from a file" do
		repository = EmployeeRepository.new "employee_data.txt"
	end

	it "should have a well know size" do
		repository = EmployeeRepository.new "employee_data.txt"
		repository.size.should == 3
	end
	it "should return first employee" do
		repository = EmployeeRepository.new "employee_data.txt"
		expected_employee = Employee.new("Doe, John, 1982/10/08, john.doe@foobar.com")
		repository.first.should == expected_employee
	end
	it "should return employees born an a given date" do
		repository = EmployeeRepository.new "employee_data.txt"
		expected_employee = Employee.new("Doe, John, 1982/10/08, john.doe@foobar.com")
		repository.born_on('2008/10/08').first.should == expected_employee
	end
end
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
require "message"
require "employee"

describe "Message" do
	before :each do
		@message = Message.new Employee.new("Doe, John, 1982/10/08, john.doe@foobar.com")
	end

	it "should have employee's email as destination" do
		@message.to.should == "john.doe@foobar.com"
	end
	it "should construct a complete body" do
		@message.body.should == "To: john.doe@foobar.com\nSubject: Happy Birthday!\n\nHappy Birthday, dear John!"
	end
end
require "message"

class BirthdayService
	def initialize(host, port)
		@host = host
		@port = port
	end
	def send_greetings(employees, date)
		employees.born_on(date).each do |employee|
			send_message(Message.new employee)
		end
	end
	def send_message(message)
    		Net::SMTP.start(@host, @port) do |smtp|
      			smtp.send_message message.body, 'your@mail.address', message.to
    		end
	end
end
class Employee
	attr_reader :firstname
	attr_reader :lastname
	attr_reader :birthdate
	attr_reader :email
	def initialize(args)
		tokens = args.split(',').map { |e| e.strip }
		@firstname = tokens[1]
		@lastname = tokens[0]
		@birthdate = tokens[2]
		@email = tokens[3]
	end

	def ==(other)
		other.instance_of?(self.class) &&
			@firstname == other.firstname &&
			@lastname == other.lastname &&
			@birthdate == other.birthdate &&
			@email == other.email
	end
end
require 'employee'

class String
	def same_day?(other)
		date1 = split('/')
		date2 = other.split('/')
		date1[1] == date2[1] && date1[2] == date2[2]
	end
end

class EmployeeRepository
	private
	def skip_header
		@employees.shift
	end

	public
	def initialize(employees_filename)
		@employees = []
		File.open(employees_filename).each_line do |line|
			@employees << Employee.new(line)
		end
		skip_header
	end

	def size
		@employees.size
	end

	def first
		@employees.first
	end

	def born_on(current_date)
		@employees.find_all { |e| e.birthdate.same_day?(current_date) }
	end
end
class Message
	def initialize(employee)
		@employee = employee
	end
	def to
		@employee.email
	end
	def body
		["To: #{to}",
		"Subject: Happy Birthday!",
		"",
		"Happy Birthday, dear #{@employee.firstname}!"].join("\n")
	end
end

Interesting I wrote more specs code than production code.

I do like I didn't used any IF, but I don't like to open String to model date and the use of split to tokenize things: I'll focus on thant in next attempt!

Technorati Tags: ,

Tags: ,