Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Integralist/9780188 to your computer and use it in GitHub Desktop.
Save Integralist/9780188 to your computer and use it in GitHub Desktop.
Object-Oriented Design Principles (Code Design)

Messages and Duck Typing (i.e. interfaces)

  • Think about "messages" not "objects".
  • "message" == "method call" (e.g. "I want to call Y's X method" == "I want to send the message X to object Y").
  • New objects should be identified from the messages you know you want to send.
  • Knowing what messages to send is facilitated by the use of "sequence diagrams".
  • The result of this way of thinking is that you rely on an "interface" and not a concrete object.
  • The reason relying on an interface is better is that the object can later be swapped for another and it doesn't matter as long as it implements the same interface (i.e. Liskov Substitution Principle).
  • When creating a new instance of an object it should cause no side effects and shouldn't complicate testing with needing to mock many dependencies. To avoid this make sure the constructor only does the minimum and any additional bootstrapping is done via another method call (e.g. 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.

Dependencies

  • "Tell, don't ask". This will allow you to reduce the "context" of your objects (e.g. the amount an object knows about another object)
  • Injecting dependencies helps to decouple objects and thus makes your objects less tightly coupled to other objects.
  • The more loosely coupled your objects, the easier it is to swap objects without requiring lots of extra code changes.
  • Don't pass a class into an object and have that receiving object instantiate the class. This is because it increases the receiving object's context; as it now knows too much about the dependency being passed to it. The object should only know the dependencies public interface (remember: "tell, don't ask"). So pass a constructed object (i.e. instantiate the class as you pass it with the message)
  • When sending a message to a dependency, pass the current object (e.g. 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).
  • In some instances loading lots of dependencies outside of a class isn't as helpful at reducing the number of dependencies in your class' constructor as you may think. The reason being: they're still dependencies; they're just "implicit" rather than "explicit". It can make testing (mocking/stubbing) easier if you're being explicit and pass a dependency into the constructor (Note: this depends on your language of choice. In Ruby, developers will argue DI isn't necessary as the language is designed to be very malleable -> so see what works for you and your language; e.g. DI Containers work well in a language such as PHP (e.g. Pimple))
  • Simplify bloated constructors by teasing out functionality into a new object and also look at related dependencies and move one dependency into another (if possible) to reduce the number of dependencies passed into a single constructor.
  • Some bloated constructor parameters might not necessarily be violating SRP. They may well receive too many dependencies but it could be that they're related to configuration and so we're not setting enough default values which can then be overridden with a single config object parameter.

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:

  • Keeping defaults within the parent
  • Keep unique behaviour within the specific child objects
  • Avoids issue where developer unfamiliar with the code would otherwise need to (or not know to) call super (by implementing 'hook methods')
  • Avoids sub classes having too much context
  • Avoids minor changes in the parent class from affecting the sub class
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

  • If you notice a Class starts to demonstrate more than one responsibility then extract that behaviour out into another Class.
@rizalmuthi
Copy link

Thank you for this. But i think you missed the default_baz on the child.
And one more thing, is it possible to provide the code sample for Dependencies.

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment