4 Simple Rules and Declarative Builders

written in

Earlier this week I bought 4 Rules of Simple Design by Corey Haines, a short but interesting discussion of the Four Rules that came out of Extreme Programming. The book is grounded in Corey’s vast experience and in the code he’s seen people write at Coderetreat. It’s a quick but meaty read. There’s a lot of knowledge in the ~65 pages of content.

In a section on testing, Haines says:

In fact, over time I’ve developed a guideline for myself that external callers can’t actually use the base constructor for an object. Put another way: the outside world can’t use new to instantiate an object with an expectation of a specific state. Instead, there must be an explicitly named factory method on the class to create an object in a specific, valid state.

I’m not sure factory is the right word here. My (admittedly incomplete) understanding is that a factory is a class that builds another class. But in the case he’s talking about we’re using a class’ method to build an instance of the same class, which I think is the Builder pattern.

Pattern pedantry aside, I tried putting this advice into practice this week and found it quite pleasing. Here’s some old code. This is from a rake task that kicks off a data processing job. This job can be configured, but in this case we’re using the default configuration.

1
2
3
4
5
6
namespace :etl do
  task :fill_queue => :environment do
    queue_filler = Etl::QueueFiller.new
    queue_filler.run
  end
end

And here’s the code afterward:

1
2
3
4
5
namespace :etl do
  task :fill_queue => :environment do
    Etl::QueueFiller.add_all_students
  end
end

And there’s a similar task that adds just some students instead of all. Before:

1
2
3
4
5
namespace :etl do
  task :add_students, [:student_ids] => :environment do |t, args|
    Etl::QueueFiller.new.add_students(student_ids)
  end
end

After

1
2
3
4
5
namespace :etl do
  task :add_students, [:student_ids] => :environment do |t, args|
    Etl::QueueFiller.add_students(student_ids)
  end
end

Minor changes, but observe how much easier this is to read. In the original examples, what value was there to new? None, as far as I can tell. All it did was return an instance that had the method I wanted. So if new has no value in this context, let’s remove it. And we’re left with a declarative statement that says exactly what I want.

In these cases I’m never working with the returned object. But I stuck with Haines’ advice throughout this entire refactoring, using it for creating objects I did work with. Deeper down in the application is the idea of a Queue Event, which we use for tracking when and why data was added to our work queue. Again, this can be configured or there is a default option.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class QueueEvent
  def self.default
    self.create_for_type("all_active_undergrads")
  end

  def self.for(type)
    self.create_for_type(type)
  end


  private

  def self.create_for_type(type)
    self.new(type: type)
  end
end

event = QueueEvent.default
event = QueueEvent.for('transfer_credits')

for may not be the best name here. Something more descriptive would be better. QueueEvent.for_type, maybe. Or, since there’s a limited number of Event types, I could probably just create explicit builders for all of them: QueueEvent.transfer_credits or QueueEvent.registration_update. Etc. Though I can see the maintenance of that being a pain. And the urge to replace these explicit builders with a bit of method_missing magic is ever present. Though I think it should be resisted. It’s too easy for method_missing to suprise and confuse other developers (or yourself) months down the road.

After trying out Corey’s advice, I don’t know that I’d stop using new all-together. Sometimes you just want a new instance of a class. But I can certainly see the value of having explict builders that give you objects in specific states. And the resulting code can be far easier to read.