factory method pattern in ruby

whatโ€™s in a pattern?

Conventional programatic design patterns come in 3 flavors ๐Ÿฆ:

But today we are going to pick on the factory pattern, which our dear friends at wikipedia describe as below.

the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created.

but for a factory the kingdom was lost

Now to dive into what a factory pattern can do for your codebase, the <label> describes it as being able to โ€œcreateโ€ Classes or Objects in a scalable & dynamic way. All the while allowing your code to be flexible, malleable, and ductile to futures changes or added features. So at a high level fly by, code written with a factory pattern in mind will be able to instantiate objects of the Classes of the problem domain it traverses.

bad spaghetti code figure_1: your spaghetti mess code won't scale
concurrency at coles figure_2: creating a common interface with `Transport`

figure_1

so below will work just fine for figure_1 but will get pretty cray cray real quick if you want to start and add extra freight types. You could try and hide your sins inside a case statement, but theyโ€™ll still be there ๐ŸงŸ.

order_data = { line001: '2 cats', line002: '3 bunnies', line003: '3 ferrets' }

class OrderGenerator
  def self.generate(order, type)
    if type == 'road'
      order.each_pair { |key, value| puts "#{key} of #{value}" }
      puts 'Delivered by ๐Ÿšš'
    elsif type == 'sea'
      order.each_pair { |key, value| puts "#{key} of #{value}" }
      puts 'Delivered by ๐Ÿ›ฅ๏ธ'
    else
      raise 'Unsupported type of transport'
    end
  end
end

OrderGenerator.generate(order_data, 'sea')
#...
line001 of 2 cats
line002 of 3 bunnies
line003 of 3 ferrets
Delivered by ๐Ÿ›ฅ๏ธ

figure_2

Here we are using the factory pattern buy instantiating the Transport Factory Class form within the existing OrderGenerator class. Now we can see that we are able to greatly extend the types of transport available to fullfil the client orders. We are also not hiding our transport types within a massive if/else or case statement.

  order_data = { line001: '2 cats', line002: '3 bunnies', line003: '3 ferrets' }

class RoadLogistics
  def deliver(order)
    return '' if order.empty?

    order.each_pair { |key, value| puts "#{key} of #{value}" }
    puts 'Delivered by ๐Ÿšš'
  end
end

class SeaLogistics
  def deliver(order)
    return '' if order.empty?

    order.each_pair { |key, value| puts "#{key} of #{value}" }
    puts 'Delivered by ๐Ÿ›ฅ๏ธ'
  end
end

class Transport
  def self.for(type)
    case type
    when 'sea'
      SeaLogistics.new
    when 'road'
      RoadLogistics.new
    else
      raise 'Unsupported type of transport'
    end
  end
end

class OrderGenerator
  def self.generate(order, type)
    Transport.for(type).deliver(order)
  end
end

OrderGenerator.generate(order_data, 'road')
#...
line001 of 2 cats
line002 of 3 bunnies
line003 of 3 ferrets
Delivered by ๐Ÿšš

We can also see that figure_2 code reflects and Open/Closed principle of the SOLID object-oriented design. So the Factory design pattern ticks a lot of boxes for just a minimal amount if initial front loaded effort.

references:

๐Ÿ‘‰ Next: concurrency, parallelism and the ractor ๐Ÿ‘ˆ Previous: state machines vs enums