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.

196ab25f16dcfd37518a41ceb15e0da0?s=128

Andy Pike

July 03, 2013
Tweet

Transcript

  1. IoC Exploring in Ruby Wednesday, 3 July 13

  2. ME Questions at the end please. @andypike ESPORTS FTW We’re

    hiring! Wednesday, 3 July 13
  3. OPEN YOUR MIND This is just a discussion, I’m interested

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

    Ruby compared to you. Wednesday, 3 July 13
  5. 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
  6. WHAT IS INVERSION OF CONTROL? Wednesday, 3 July 13

  7. MISCONCEPTIONS It’s not about XML! Wednesday, 3 July 13

  8. 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
  9. 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
  10. 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
  11. WHAT ABOUT DEPENDENCY INJECTION? Wednesday, 3 July 13

  12. 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
  13. 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
  14. 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
  15. TO ME, IOC IS JUST GOOD OOP IRRESPECTIVE OF LANGUAGE

    Wednesday, 3 July 13
  16. WHY DON’T RUBYISTS DO IOC? Or at least seem not

    to. Well, I asked some... Wednesday, 3 July 13
  17. MODULES ...their answer. Wednesday, 3 July 13

  18. 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
  19. 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
  20. 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
  21. MODULES AND MAGIC POWERS Wednesday, 3 July 13

  22. 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
  23. IT’S LIKE THIS Not saying mixins are bad - they’re

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

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

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

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

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

    - they’re awesome, just not always the answer. Wednesday, 3 July 13
  29. 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
  30. 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
  31. FOR ARGUMENTS SAKE, LET’S TAKE CONSTRUCTOR INJECTION FUTHER... Wednesday, 3

    July 13
  32. 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
  33. 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
  34. ENTER THE IOC CONTAINER Wednesday, 3 July 13

  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. HOW DOES THAT WORK? Wednesday, 3 July 13

  43. 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
  44. TAKE IT FURTHER. HOW ABOUT CONSTRUCTOR INJECTION WITH RAILS CONTROLLERS?

    Wednesday, 3 July 13
  45. 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
  46. 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
  47. 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
  48. 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
  49. LEGO vs PLAY-DOH Rigid vs Pliable Java/C# Ruby Wednesday, 3

    July 13
  50. LEGO vs PLAY-DOH Just be careful. A little bit of

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

    should (all the time) Wednesday, 3 July 13
  52. 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
  53. 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
  54. QUESTIONS & COMMENTS @andypike youtube.com/watch?v=7T0vs9gYydo +/- Feedback welcome: speakerrate.com/andypike Sorry,

    no Star Wars questions. Wednesday, 3 July 13