- 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 thebootstrap
code inside the constructor). If your code is justFoo.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.
- "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.
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 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
- If you notice a Class starts to demonstrate more than one responsibility then extract that behaviour out into another Class.
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