Another Refactoring Story

TLDR; Keeping with the refactoring stories started with My GPA in Code Climate is 3.59: A Refactoring Story I decided today to get a piece of code as much in shape as my knowledge let me, of course I’m always waiting for suggestions on how to improve so please do leave comments. 

This code base is part of an exercise from the thoughtbot learning program Learn Prime which have great content go ahead and check it out you won’t regret it. So getting that out of the way, let’s see the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class SurveyInviter
     EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

     def initialize(attributes = {})
          @survey = attributes[:survey]
          @message = attributes[:message] || ''
          @recipients = attributes[:recipients] || ''
          @sender = attributes[:sender]
     end

     attr_reader :message, :recipients, :survey

     def valid?
          valid_message? && valid_recipients?
     end

     def deliver
          recipient_list.each do |email|
               invitation = Invitation.create(
                    survey: @survey,
                    sender: @sender,
                    recipient_email: email,
                    status: 'pending'
               )
               Mailer.invitation_notification(invitation, @message)
          end
     end

     def invalid_recipients
          @invalid_recipients ||= recipient_list.map do |item|
               unless item.match(EMAIL_REGEX)
                    item
               end
          end.compact
     end

private

     def valid_message?
          @message.present?
     end

     def valid_recipients?
          invalid_recipients.empty?
     end

     def recipient_list
          @recipient_list ||= @recipients.gsub(/\s+/, '').split(/[\n,;]+/)
     end
end

In the other article regarding refactoring I used Flog which is quite handy so let’s use it with this one too:

1
2
3
4
5
6
27.3: flog total
3.4: flog/method average

6.8: SurveyInviter#initialize lib/survey_inviter.rb:17
5.6: SurveyInviter#invalid_recipients lib/survey_inviter.rb:42
4.9: SurveyInviter#deliver lib/survey_inviter.rb:30

Surprise; that looks like a pretty solid report. Following my own rule regarding 10 or less on flog which is good. Which means that I’m done, right? Of course not this is a refactoring post so I have to at least rename a variable. Let’s use another tool and see what it says this time I’m going to use Reek.

1
2
 reek lib/survey_inviter.rb

Surprise again; so there’s no warnings from reek regarding any code smell. So I think now I’m done, right? Well again I need to do something with this code. But I will stop just using tools I will just create some constraints for myself and try to shape the code on that; the constraints are basically this list: Object Calisthenics

Only One Level Of Indentation Per Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class SurveyInviter
     EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

     def initialize(attributes = {})
          @survey = attributes[:survey]
          @message = attributes[:message] || ''
          @recipients = attributes[:recipients] || ''
          @sender = attributes[:sender]
     end

     attr_reader :message, :recipients, :survey

     def valid?
          valid_message? && valid_recipients?
     end

     def deliver
          recipient_list.each do |email|
               Mailer.invitation_notification(invitation(email), @message)
          end
     end

     def invalid_recipients
          @invalid_recipients ||= recipient_list.map do |item|
               item unless item.match(EMAIL_REGEX)
          end.compact
     end

     private

     def invitation(email)
          Invitation.create(
               survey: @survey,
               sender: @sender,
               recipient_email: email,
               status: 'pending'
          )
     end

     def valid_message?
          @message.present?
     end

     def valid_recipients?
          invalid_recipients.empty?
     end

     def recipient_list
          @recipient_list ||= @recipients.gsub(/\s+/, '').split(/[\n,;]+/)
     end
end

Don’t Use The ELSE Keyword

This code doesn’t have else keyword so we can keep on without changing it.

 Wrap All Primitives And Strings and First Class Collections

So even when we have the notion of Everything is an Object in Ruby still I would count types like Fixnum, String and Array as primitives for the following changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class SurveyInviter

     def initialize(attributes = {})
          @survey = attributes[:survey]
          @message = Message.new(attributes[:message])
          @recipients = Recipients.new(attributes[:recipients])
          @sender = attributes[:sender]
     end

     attr_reader :message, :recipients, :survey
     def valid?
          message.valid? && recipients.valid?
     end

     def deliver
          recipients.each do |recipient|
               Mailer.invitation_notification(invitation(recipient), message.to_s)
          end
     end

     private
     def invitation(email)
          Invitation.create(
               survey: @survey,
               sender: @sender,
               recipient_email: email,
               status: 'pending'
          )
     end
end

class Message
     def initialize(message)
          @message = message || ''
          freeze
     end

     def valid?
          @message.present?
     end

     def to_s
          @message
     end
end

class Recipients
     EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

     def initialize(recipients)
          @recipients = build_recipient_list(recipients || '')
          freeze
     end

     def valid?
          invalid_recipient_list.empty?
     end

     def each
          return enum_for(:each) unless block_given?

          @recipients.each do |recipient|
               yield recipient
          end
     end

     private
     def build_recipient_list(recipients)
          recipients.gsub(/\s+/, '').split(/[\n,;]+/)
     end

     def invalid_recipient_list
          @recipients.map do |item|
               item unless item.match(EMAIL_REGEX)
          end.compact
     end
end

One Dot Per Line

I just found one method which more than one dot in the same line and that’s the only change in here so I will just show that particular class. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Recipients
  EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

  def initialize(recipients)
    @recipients = build_recipient_list(recipients || '')
    freeze
  end

  def valid?
    invalid_recipient_list.empty?
  end

  def each
    return enum_for(:each) unless block_given?

    @recipients.each do |recipient|
      yield recipient
    end
  end

  private
  def build_recipient_list(recipients)
    remove_space(recipients).split(/[\n,;]+/)
  end

  def remove_space(recipients)
    recipients.gsub(/\s+/,'')
  end

  def invalid_recipient_list
    @recipients.map do |item|
      item unless item.match(EMAIL_REGEX)
    end.compact
  end
end

Don’t Abbreviate

I don’t have any abbreviations at the moment on this particular piece of code and most name make sense to me. Probably I’m wrong but that’s my perception at the moment. Btw this constraint mean not to shrink method names, variables and constants. 

Keep All Entities Small

So the rule says that no class should contain more than 50 lines and no package should contain more than 100 files. Both things are taking care of now; the only thing that I did at the end was to move the classes to it’s own files. 

No Classes With More Than Two Instance Variables

This was kinda hard; but it just points me to the fact that I did not follow rule number 3 properly; because not every primitive is wrapped. The advantage of this rule is that force you to encapsulate and distinguish classes that do orchestration which does that work as containers. Which reminds me the distintion in the Book Growing Object Oriented Design Guided by Tests between objects and values. Of course the number of instance variables is arbitrary but I think is a good number that help me stretch my brain a little bit ;–)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class SurveyInviter
 
     def initialize(attributes = {})
          @survey_manager = SurveyManager.new(
               Survey.new(attributes[:survey]),
               Sender.new(attributes[:sender])
          )
          @message_manager = MessageManager.new(
               Message.new(attributes[:message]),
               Recipients.new(attributes[:recipients])
          )
     end
 
     def valid?
          message_manager.is_message_valid? && message_manager.are_recipients_valid?
     end
 
     def deliver
          message_manager.all_recipients do |recipient|
               Mailer.invitation_notification(invitation(recipient), message_manager.message)
          end
     end
 
     private
     attr_reader :message, :recipients, :survey, :message_manager
 
     def invitation(email)
          Invitation.create(
               survey: @survey_manager.survey,
               sender: @survey_manager.sender,
               recipient_email: email,
               status: 'pending'
          )
          end
     end
end

class MessageManager

  def initialize(message, recipients)
    @message = message
    @recipients = recipients
    freeze
  end

  def all_recipients(&block)
    @recipients.each(&block)
  end

  def is_message_valid?
    @message.valid?
  end

  def are_recipients_valid?
    @recipients.valid?
  end

  def message
    @message.to_s
  end

end

class SurveyManager
  def initialize(survey, sender)
    @survey = survey
    @sender = sender
    freeze
  end

  def survey
    @survey.to_s
  end

  def sender
    @sender.to_s
  end
end

No Getters/Setters/Properties

This particular rule in fact refers to Tell Don’t Ask which means that is fine to use accessors to know an object’s inside state but not to make any decisions with that data outside the object. So basically I just remove the attr_accessors that I had in the SurveyInviter class just because I don’t need them any more and that will expose data that I really don’t need to use outside of the class. 

Conclusion 

So there you have it; another happy ending for a refactoring story; I learned a lot from this one, hope I shared as much. Enjoy.

Comments