I’ve been using Ruby professionally for close to three years now. I’ve recently gotten more curious into what different patterns are called and how they are used in other programming languages as well as what typical programming patterns can’t be used in Ruby.
When I create a new service object in my Rails code, with only one purpose, I tend to still write it as an instance object, so that the garbage collector can pick it up.
Let say we have this code:
class Mailer
attr_reader :type
def initialize(type)
@type = type
end
def send_mail!
ExternalMailClient.send_mail(type)
end
end
to call this, we’d have to do:
Mailer.new(:my_type).send_mail!
When the external call in #send_mail! is done, the new Mailer instance will be marked for deletion and picked up by the garbage collector. But that new followed by send_mail! thorns in my eye. I tend to do this instead:
class Mailer
class << self
def send_mail!(type)
new(type).send_mail!
end
end
attr_reader :type
def initialize(type)
@type = type
end
def send_mail!
ExternalMailClient.send_mail(type)
end
end
Now I can instead do:
Mailer.send_mail!(:my_type)
and I don’t have to worry about the new instance or the fact that it is indeed creating a new instance of the Mailer class underneath the hood.
You could even go as far as adding:
private :new
to the class definition, so no one can call it without using your #send_mail! method.
My question is simple: What is this pattern called?
I’m no expert on terminology. But I’m not sure this has a pattern name yet.
I try to use a module for single function use cases. So personally I’d probably write it more like:
module Mailer
class << self
def send_mail!(type)
ExternalMailClient.send_mail(type)
end
end
end
There is only one Mailer Object that stays around and the parameter is the only garbage collected item. Of course this looks like it’s just aliasing ExternalMailClient to Mailer. In which case this may work.
Mailer = ExternalMailClient
And instead of the bang method I’d just use the standard method :send_mail.
The thing that Mailer is solving here, is being an instance of something, with an internal state, however it is created and used for a single purpose and then thrown away.
Yeah I know what you mean about mailer being an instance with state. I don’t like how it’s implemented. In my mind an email shouldn’t know about sending. I think a Mail object should hold the state and a Mailer be purely functional.
I have a new pattern I’m using! I’m translating a Python library and their structs are nothing like our structs. But everything in Ruby is an Object so I’m cleanly inheriting with keyward args and dynamically define the instance variables.
Here’s a snippet of the code
module NumberTypes
class NumFlags
@@attrs = [:bytewidth, :min_val, :max_val, :rb_type, :name, :packer_type]
attr_accessor *@@attrs
def initialize **opts
@value = 0 # YES/NO/I DON'T KNOW MAYBE NIL ;-)
@@attrs.each do |a|
instance_variable_set "@#{a}", opts.fetch(a) {nil}
end
end
def self.inherited base
def base.rb_type value
new.instance_exec {@value = value; self}
end
end
def coerce other
[other, @value]
end
end
module ::Boolean; end
FalseClass.prepend Boolean
TrueClass. prepend Boolean
class BoolFlags < NumFlags
def initialize **opts
super( {
bytewidth: 1,
min_val: false,
max_val: true,
rb_type: Boolean,
name: "bool",
packer_type: nil
}.
update opts)
end
end
class Uint8Flags < NumFlags
def initialize **opts
super( {
bytewidth: 1,
min_val: 0,
max_val: (2**8) - 1,
rb_type: Integer,
name: "uint8",
packer_type: nil
}.
update opts)
end
end
class Uint16Flags < NumFlags
def initialize **opts
super( {
bytewidth: 2,
min_val: 0,
max_val: (2**16) - 1,
rb_type: Integer,
name: "uint16",
packer_type: nil
}.
update opts)
end
end
end
I currently have the packer_type as nil because I haven’t figured out what behavior I need to imitate there. But I thought you all might like my design pattern for inheritance so I thought I’d share it here.
The way new instances were created in Python structs with py_type is pretty weird. I mimicked the behavior. Also why coerce was needed.