« Back to Index

Object-Oriented Design Principles (Code Design)

View original Gist on GitHub

Object-Oriented Design Principles (Code Design).md

Messages and Duck Typing (i.e. interfaces)

Dependencies

Patterns

Template Method Pattern

Use the ‘Template Method Pattern’ with inheritance to abstract away common code into parent class. Some other things this pattern helps improve is:

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

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

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.

Null Object Pattern

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

Replace Conditional with Polymorphism

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);

Transform complex data structures

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

Miscellaneous