Home > Articles > Programming > Ruby

  • Print
  • + Share This
From the author of

Refactor Your Code To Make It Maintainable

The obvious things to fix immediately are the hard-coded mail server name and email address. We also need to break out sending the email from the code that finds the runners to send the email to.

Before doing this refactoring, check the original, untidy code into your source control system—just to be on the safe side. After all, if you're shuffling a lot of code around there's always the chance that you might make a mistake.

require 'net/smtp'
class Club
 attr_accessor :members
 def initialize(mail_server, club_email)
  @members = Array.new
  @email = club_email
  @smtp = Net::SMTP.new(mail_server)
  @smtp.start

 end
 def add (runner)
  members << runner
 end
 def send_notice (evt, str, &comparison)
  sent = 0
  members.each {|r|
   selected = r.matches?(str, &comparison)
   if nil != selected then
     send_email(evt, selected )
     sent += 1
   end
  }
  return sent
 end
 def send_email(evt, r)
  @smtp.ready(@email, r.email) { |msg|
   msg.write "To: #{r.name} <#{r.email}>\r\n"
   msg.write "Subject: #{evt.name} on #{evt.date.to_s}\r\n"
   msg.write "\r\n"
   evt.description.each { |str|
    msg.write str + "\r\n"
   }
  }
  return true
 end
end

TC_Club.rb also needs a slight change to pass in the name of the mail server and the club's email address:

@club = Club.new("localhost", "secretary@club.ca")

Once the mainline code is working, you need to look at all of the weird and wonderful failures identified in the use case and acceptance test cases to make sure that the code will handle them correctly. To do that, the code needs to do some exception handling and separate selecting the runners from sending them email (otherwise we can't show how many members have been selected).

First we need to specify an extra test case for the new behavior—that of selecting a set of runners:

def test_get_selected_members
 array = @club.get_selected_members("Pete") {
  |r,n| if n == r.name then r else nil end}
 assert_equal(1, array.size, "wrong number selected")
end

And then the Club class has to be refactored to extract the selection method:

class Club
#... other parts omitted
 def get_selected_members (str, &comparison)
  ary = Array.new
  members.each {|r|
   selected = r.matches?(str, &comparison)
   if nil != selected then
     ary << selected
   end
  }
  return ary
 end
 def send_notice (evt, str, &comparison)
  sent = 0
  runners = get_selected_members(str, &comparison)
  runners.each {|runner|
   if send_email(evt, runner) then
    sent += 1
   end
  }
  return sent
 end
#... other parts omitted
end

This separation of selection from sending email allows the application to display the selected members before the emails are sent. Without the separation, the club secretary couldn't confirm that the right members have been selected.

The application also needs exception handling to deal with runtime errors that can be reported from the SMTP methods. If the mail server is unreachable in any way, or reports an error, the code will trap it so that it can be reported gracefully.

The rescue clause in the initialize method traps against things like unreachable mail servers. It simply prints the error message and clears out the smtp variable so that no further errors are reported. If the mail server is unreachable, you'll see a message something like this:

Errno::E10061 Unknown Error - \"connect(2)\"

The rescue clause in the send_email method traps error messages returned from the mail server. It simply prints the error message and returns false to indicate that the email was not sent. A possible error message back from the mail server is as follows:

Net::ProtoFatalError 550 550 5.7.1 Mail relay not allowed at this server
class Club
 attr_accessor :members
 def initialize(mail_server, club_email)
  @members = Array.new
  @email = club_email
  begin
   @smtp = Net::SMTP.new(mail_server)
   @smtp.start
  rescue Exception => err
   p err.class.to_s + " " + err.to_s
   @smtp = nil
  end
 end
#... omitted parts
 def send_email(evt, r)
  if nil == @smtp then return false end
  begin
  @smtp.ready(@email, r.email) { |msg|
   msg.write "To: #{r.name} <#{r.email}>\r\n"
   msg.write "Subject: #{evt.name} on #{evt.date.to_s}\r\n"
   msg.write "\r\n"
   evt.description.each { |str|
    msg.write str + "\r\n"
   }
  }
  rescue Exception => err
   p err.class.to_s + " " + err.to_s
   return false
  end
  return true
 end
end

So there we have it: a very simple use of Ruby to send emails. Obviously, the application itself is far from complete; all we have so far is three domain classes and their associated unit tests. We still need to design a user interface for this application to work with these domain classes and also design the data storage, either using files or a database.

Hopefully, by this stage you're getting a sense for the complexity that you'll have to deal with when creating software. Gracefully handling all of the weird and wonderful things that can go wrong is the key to successful software development.

  • + Share This
  • 🔖 Save To Your Account