Foo.new.bootup
rather than having all of the bootstrap
code inside the constructor). If your code is just Foo.new
and lots of things start to happen then that’s a recognised code smell because your constructor isn’t just doing some configuration; it’s actually actioning and sending messages.self
) along with the message. This will further decouple your objects (i.e. the objects either side of a message can be easily swapped as neither relies on a concrete object).Use the ‘Template Method Pattern’ with inheritance to abstract away common code into parent class. Some other things this pattern helps improve is:
super
(by implementing ‘hook methods’)class Parent
attr_reader :foo, :bar
def initialize(args = {})
@foo = args[:foo] || default_foo
@bar = args[:bar] || default_bar
post_initialize
end
def merge_obj(obj)
obj.merge(new_obj)
end
protected
def default_foo
"foo"
end
def default_bar
"bar"
end
def default_baz
raise NotImplementedError # this protects us from forgetting to implement an required method
end
def post_initialize
nil
end
def new_obj
{}
end
end
class Child < Parent
def post_initialize
# what would have been in a `super` call
# is now placed here and used as a 'hook method'
puts "Child specific stuff (that normally would have been inside the constructor) goes here"
end
private
def default_foo
"FOO!"
end
def default_bar
"BAR!"
end
def new_obj
# override the `new_obj` method within the parent class
{ :foo => :bar }
end
end
# The following is example usage of the above code...
parent = Parent.new
puts parent.merge_obj(:a => 1, :b => 2, :c => 3)
puts parent.foo
puts parent.bar
child = Child.new
puts child.merge_obj(:d => 4, :e => 5, :f => 6)
puts child.foo
puts child.bar
Hook methods don’t work that well with deep hierarchy class structures. Best to avoid and use some form of composition (typically via module inclusion) to build up your functionality.
Composition and Aggregation both effectively mean the same thing: composing objects from other objects.
But there is a subtle difference between them, which is that Aggregation refers to composing objects which have a life (e.g. they continue to exist and have relevance) outside of the object they’re being composited within.
Typically you’ll use the term composition nearly all the time unless you have a specific reason to provide a really granular explanation of the system you’re designing.
Rather than implementing multiple checks for available properties throughout your code; instead introduce the ‘Null Object Pattern’.
class RealObject
def a
"A!"
end
def b
"B!"
end
end
class NullObject
def a
"Default value for A"
end
def b
"Default value for B"
end
end
class Bar
def initialize(obj)
@thing = obj || NullObject.new
end
def do_the_thing
puts @thing.a
puts @thing.b
end
end
bar1_passes_object = Bar.new(RealObject.new)
bar2_doesnt_pass_object = Bar.new
bar1_passes_object.do_the_thing # => A!, B!
bar2_doesnt_pass_object.do_the_thing # => Default value for A, Default value for B
Instead of using conditionals (e.g. if/else
or switch/case
) use Polymorphism. This really means: “use a consistent interface between all your objects”.
# Bad...
class Foo
def initialize(data)
@data = data
end
def do_something
if @data.class == Bar
puts "Bar!"
elsif @data.class == Baz
puts "Baz!"
elsif @data.class == Qux
puts "Qux!"
end
end
end
class Bar; end
class Baz; end
class Qux; end
foo_bar = Foo.new(Bar.new)
foo_bar.do_something
foo_baz = Foo.new(Baz.new)
foo_baz.do_something
foo_qux = Foo.new(Qux.new)
foo_qux.do_something
# Good (Polymorphism)...
class Foo
def initialize(data)
@data = data
end
def do_something
@data.identifier
end
end
class Bar
def identifier
puts "#{self.class}!"
end
end
class Baz
def identifier
puts "#{self.class}!"
end
end
class Qux
def identifier
puts "#{self.class}!"
end
end
foo_bar = Foo.new(Bar.new)
foo_bar.do_something
foo_baz = Foo.new(Baz.new)
foo_baz.do_something
foo_qux = Foo.new(Qux.new)
foo_qux.do_something
Here’s a JavaScript implementation:
// Bad...
function test (condition) {
if (condition === "A") {
// lots of code related to "A" here
} else if (condition === "B") {
// lots of code related to "B" here
} else if (condition === "C") {
// lots of code related to "C" here
}
}
test('A');
test('B');
test('C');
// Good (Polymorphism)......
var A = {
doTheThing: function(){
lots of code related to "A" here
}
}
var B = {
doTheThing: function(){
lots of code related to "B" here
}
}
var C = {
doTheThing: function(){
lots of code related to "C" here
}
}
function test (condition) {
condition.doTheThing();
}
test(A);
test(B);
test(C);
Avoid trying to access complex data structures. In the following example we convert a complex and indecipherable Array into an object with a clearly defined set of methods which helps clarify the code.
# Bad...
class Foo
attr_reader :data
def initialize(data)
@data = data
end
def do_something
data.each do |item|
puts item[0]
puts item[1]
puts '---'
end
end
end
obj = Foo.new([[10, 25],[3, 9],[41, 7]])
obj.do_something
# Good...
class Foo
attr_reader :new_data
def initialize(data)
@new_data = transform(data)
end
def do_something
new_data.each do |item|
# now we are able to reference easily understandable
# property names (rather than item[0], item[1])
puts item.coord_x
puts item.coord_y
puts '---'
end
end
Transform = Struct.new(:coord_x, :coord_y)
def transform(data)
data.collect { |item| Transform.new(item[0], item[1]) }
end
end
obj = Foo.new([[10, 25],[3, 9],[41, 7]])
obj.do_something