Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring IoC in Ruby

Exploring IoC in Ruby

My thoughts on IoC containers in Ruby for Surrey Rubyists User Group in July 2013.

Andy Pike

July 03, 2013
Tweet

More Decks by Andy Pike

Other Decks in Programming

Transcript

  1. OPEN YOUR MIND This is just a discussion, I’m interested

    in what you think. Wednesday, 3 July 13
  2. MY BACKGROUND IS C# Yes, static languages. Relatively new to

    Ruby compared to you. Wednesday, 3 July 13
  3. FAT MODELS Here is where it all started. • I

    never really liked skinny controller fat model • Started splitting models into modules • Now using service objects, presenters, etc • This led to thinking about IoC as we do in .NET Wednesday, 3 July 13
  4. CLASS HAS CONTROL Class method and .new is hardcoding if

    that’s possible in a dynamic language class Library def send_reminders Borrowing.overdue.each do |borrowing| MemberMailer.overdue_notice(borrowing).deliver borrowing.reminder_sent! end end end Wednesday, 3 July 13
  5. CALLER HAS CONTROL Inverted control, class no longer has control

    class Library def send_reminders(notifier) Borrowing.overdue.each do |borrowing| notifier.send_overdue_notice(borrowing) borrowing.reminder_sent! end end end Collaborator is passed in Wednesday, 3 July 13
  6. WHY IS THIS IMPORTANT? • Loosely coupled • Easy to

    change by passing in different collaborators • Helps with open-closed principle • Better tests (not a reason but positive side effect) Wednesday, 3 July 13
  7. METHOD INJECTION class Library def send_reminders(notifier) # notifier... end end

    notifier = EmailNotifier.new library = Library.new library.send_reminders(notifier) Nice and simple, collaborators created and injected where needed. Problem with duplication if collaborator used in multiple methods Wednesday, 3 July 13
  8. ATTRIBUTE INJECTION class Library attr_accessor :notifier def send_reminders # @notifier...

    end end notifier = EmailNotifier.new library = Library.new library.notifier = notifier library.send_reminders Simple, collaborators can be reused across methods. What if the attribute is not set? Wednesday, 3 July 13
  9. CONSTRUCTOR INJECTION class Library def initialize(notifier) @notifier = notifier end

    def send_reminders # @notifier... end end notifier = EmailNotifier.new library = Library.new(notifier) library.send_reminders As collaborators are a dependency, enforces injection and allows reuse. Big object graphs are a pain to create. Wednesday, 3 July 13
  10. WHY DON’T RUBYISTS DO IOC? Or at least seem not

    to. Well, I asked some... Wednesday, 3 July 13
  11. MODULES class Library include EmailNotifier def send_reminders # send_overdue_notice(borrowing) end

    end library = Library.new library.send_reminders Easy, no collaborators to create but hardcoded in this case. Wednesday, 3 July 13
  12. MODULES II module Notifier def send_overdue_notice(borrowing) # send email or

    SMS end end class Library include Notifier def send_reminders # send_overdue_notice(borrowing) end end Not so hardcoded. Change module implementation to switch strategy Wednesday, 3 July 13
  13. MODULES III class Library def send_reminders # send_overdue_notice(borrowing) end end

    class Library include EmailNotifier end Reopen class and include correct notifier Wednesday, 3 July 13
  14. MAGIC POWERS • Use modules where it makes sense to

    enhance multiple classes • Don’t use a module when the behaviour should be in its own class • Single Responsibility Principle - does that count after mixin or before? • Don’t use Modules in one class just used to break down file size Wednesday, 3 July 13
  15. IT’S LIKE THIS Not saying mixins are bad - they’re

    awesome, just not always the answer. Wednesday, 3 July 13
  16. IT’S LIKE THIS Not saying mixins are bad - they’re

    awesome, just not always the answer. Wednesday, 3 July 13
  17. IT’S LIKE THIS + Not saying mixins are bad -

    they’re awesome, just not always the answer. Wednesday, 3 July 13
  18. IT’S LIKE THIS + Not saying mixins are bad -

    they’re awesome, just not always the answer. Wednesday, 3 July 13
  19. IT’S LIKE THIS + = Not saying mixins are bad

    - they’re awesome, just not always the answer. Wednesday, 3 July 13
  20. IT’S LIKE THIS + = Not saying mixins are bad

    - they’re awesome, just not always the answer. Wednesday, 3 July 13
  21. TESTING & MOCKING class Foo def hello time = Time.now.strftime('%H:%M')

    "Hello, the time is #{time}" end end describe Foo it "says hello with the current time" do Time.stub(:now){ Time.new(2013, 6, 20, 12, 0, 0) } subject.hello.should == "Hello, the time is 12:00" end end We have powerful tools but is this what we should do? Wednesday, 3 July 13
  22. TESTING & MOCKING class Foo def hello(clock) "Hello, the time

    is #{clock.now}" end end describe Foo it "says hello with the current time" do clock = stub(now: "12:00") subject.hello(clock).should == "Hello, the time is 12:00" end end IoC simplifies testing :) Allow us to just use dumb stubs Wednesday, 3 July 13
  23. SOME CLASSES class Nails def to_s "nails" end end class

    Glue def to_s "glue" end end class HandSaw def to_s "hand saw" end end class Electricity def to_s "electrical" end end class Carpenter def initialize(tool, fixings) @tool = tool @fixings = fixings end def build_something puts "Building something in wood with #{@fixings} and #{@tool}" end end class PowerSaw def initialize(power_source) @power_source = power_source end def to_s "#{@power_source} power saw" end end Wednesday, 3 July 13
  24. INJECTING OBJECT GRAPH fixings = Nails.new power_source = Electricity.new tool

    = PowerSaw.new(power_source) craftsman = Carpenter.new(tool, fixings) craftsman.build_something # => Building something in wood with nails and electrical power saw Separate class for each responsibility and can easily change strategies but creating all these instances is a pain. Wednesday, 3 July 13
  25. WHAT IS AN IOC CONTAINER • Register implementations against a

    key • Allows registering of components in a single place • Instantiates registered implementations • Recursively resolves all dependencies • Injects dependencies • Allows environment specific implementations • Manages component lifecycle Wednesday, 3 July 13
  26. MY IOC CONTAINER RULES • Classes should not be aware

    of the container (PORO) • Should be built into the framework • Configure using code not XML • When using, only resolve the root object Wednesday, 3 July 13
  27. IOC CONTAINERS IN C# public interface ITool { string Name

    { get; } } public class HandSaw : ITool { public string Name { get { return "Hand Saw"; } } } Wednesday, 3 July 13
  28. IOC CONTAINERS IN C# public interface ICraftsman { void BuildSomething();

    } public class Carpenter : ICraftsman { private ITool tool; public Carpenter(ITool tool) { this.tool = tool; } public void BuildSomething() { Console.WriteLine("Building something in wood with a {0}", tool.Name); } } Code to interfaces Wednesday, 3 July 13
  29. IOC CONTAINERS IN C# container .Register(Component.For<ICraftsman>().ImplementedBy<Carpenter>()) .Register(Component.For<ITool>().ImplementedBy<HandSaw>()); var craftsman =

    container.Resolve<ICraftsman>(); craftsman.BuildSomething(); Register the components against an interface. Resolve the root interface. Wednesday, 3 July 13
  30. IOC CONTAINERS IN RUBY • Most break the PORO rule

    to get around the lack of interfaces: container.register(:tool, HandSaw) class Carpenter inject :tool def build_something puts “Building something in wood with a #{@tool.name}“ end end Yuck Wednesday, 3 July 13
  31. MY RUBY CONTAINER Injectr.create_container do |c| c.register :craftsman, Carpenter c.register

    :tool, PowerSaw c.register :fixings, Nails c.register :power_source, Electricity end craftsman = Injectr.resolve(:craftsman) craftsman.build_something # => Building something in wood with nails and electrical power saw Register your components with the container then ask for the root and all dependancies will be instantiated automatically Wednesday, 3 July 13
  32. MY ATTEMPT class Injectr attr_reader :registry def self.create_container @container =

    self.new yield @container @container end def initialize @registry = {} end def register(key, klass) @registry[key] = klass end def self.resolve(key) klass = @container.registry[key] create(klass) end def self.create(klass) constructor_params = klass.instance_method(:initialize).parameters dependancies = constructor_params.map{|p| resolve(p.last)} klass.new(*dependancies) end end Recursively resolves objects using the constructor param names as the key Wednesday, 3 July 13
  33. CREATING CONTROLLERS # See github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal.rb#L229 module ActionController # ... class

    Metal < AbstractController::Base def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) end end end end Instantiates the controller here with no args. Self is your controller class. Wednesday, 3 July 13
  34. MY MONKEY PATCH module ActionController class Metal < AbstractController::Base def

    self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| Injectr.create(self).dispatch(name, klass.new(env)) end end end end Create an instance of the controller and inject any constructor dependancies Wednesday, 3 July 13
  35. A CONTROLLER Put the registration block in an initializer. Must

    call super(). class HomeController < ApplicationController def initialize(craftsman) super() @craftsman = craftsman end def index render :text => @craftsman.build_something end end $ curl http://localhost:3000 => Building something in wood with nails and electrical power saw Wednesday, 3 July 13
  36. INJECTR GEM Not production ready. Use at own risk. •

    Check it out: github.com/andypike/injectr • What’s missing: • Handle missing registrations • Circular dependency detection • Optional params • Lifecycle options Wednesday, 3 July 13
  37. LEGO vs PLAY-DOH Just be careful. A little bit of

    order is ok. Ruby Ruby Wednesday, 3 July 13
  38. COULD VS SHOULD Just because we can, doesn’t mean we

    should (all the time) Wednesday, 3 July 13
  39. CONCLUSIONS • IoC is just good OOP and you should

    be doing it • There might be a place for containers (sometimes) • Just because you can, doesn’t mean you should Wednesday, 3 July 13
  40. MY QUESTIONS TO YOU • Am I just over complicating

    it all? • What have I misunderstood? • How do Rubyists think about this stuff? One of the reasons for this talk is to help me! Wednesday, 3 July 13