$30 off During Our Annual Pro Sale. View Details »

The Ruby Module Builder Pattern

The Ruby Module Builder Pattern

Talk at RubyKaigi 2017 about the Module Builder Pattern.

Video: https://www.youtube.com/watch?v=_E1yKPC-r1E
Blog Post: http://dejimata.com/2017/5/20/the-ruby-module-builder-pattern

Chris Salzberg

September 19, 2017
Tweet

More Decks by Chris Salzberg

Other Decks in Programming

Transcript

  1. Ruby
    The Module Builder Pattern
    The Module Builder Pattern
    Chris Salzberg
    ^

    View Slide

  2. About Me

    Writer, programmer, translator

    @shioyama on github, twitter, SO

    I program in Ruby

    I live in Tokyo

    I am the author of Mobility

    I blog at dejimata.com

    View Slide

  3. View Slide

  4. We’re Hiring!

    View Slide

  5. Introduction
    Introduction

    View Slide

  6. View Slide

  7. MODULE

    View Slide

  8. MODULE
    What the heck is a
    anyway?

    View Slide

  9. “A self-contained component of a system,
    often interchangeable, which has a well-
    defined interface to the other components.”

    View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. Modular design is an attempt to combine the advantages
    of standardization with those of customization.
    - Wikipedia
    Modular Design

    View Slide

  15. Elements of Module Design

    View Slide

  16. Elements of Module Design

    Abstraction
    Extract common pattern into functional unit

    View Slide

  17. Elements of Module Design

    Abstraction
    Extract common pattern into functional unit

    Specification
    Define contractual interface for unit

    View Slide

  18. Elements of Module Design

    Abstraction
    Extract common pattern into functional unit

    Specification
    Define contractual interface for unit

    Encapsulation
    Hide implementation behind public interface

    View Slide

  19. Scaling Rule
    The broader the scope of your abstraction
    the more expressive your specification must be
    the tighter your encapsulation must be

    View Slide

  20. Modularity in Ruby

    Module (modularity through inclusion)
    – collection of methods and constants
    – callback method (included)

    Class (modularity through inheritance)
    – initializer, methods, constants
    – callback method (inherited)

    View Slide

  21. Gems are also Modules!

    Self-contained component
    – collection of Classes, Modules, Objects and code

    Well-defined interface to other components
    – gem/ruby dependencies (gemspec), class/object interfaces

    As flexible as its components allow it to be
    gem install degica

    View Slide

  22. A Modular Gem: Mobility

    Pluggable “Translation Framework”

    Abstracts storage strategies into “backends”

    Supports a flexible collection of plugins
    Plugins
    Backend

    View Slide

  23. The Ruby Module
    module MyModule
    def foo
    “foo”
    end
    def self.included(base)
    puts “included in #{base.name}”
    end
    end
    module MyModule
    def foo
    “foo”
    end
    def self.included(base)
    puts “included in #{base.name}”
    end
    end
    class A
    include MyModule
    def foo
    super + “bar”
    end
    end
    # prints “included in A”
    a = A.new
    a.foo
    #=> “foobar”
    class A
    include MyModule
    def foo
    super + “bar”
    end
    end
    # prints “included in A”
    a = A.new
    a.foo
    #=> “foobar”

    View Slide

  24. Flexible? Nope.
    module MyModule(config)
    def foo
    config
    end
    end
    module MyModule(config)
    def foo
    config
    end
    end

    View Slide

  25. Flexible? Nope.
    module MyModule(config)
    #=> SyntaxError: unexpected '\n'...
    def foo
    config
    end
    end
    module MyModule(config)
    #=> SyntaxError: unexpected '\n'...
    def foo
    config
    end
    end
    Class A
    include MyModule(config)
    end
    #=> NoMethodError: undefined method `MyModule' for A:Class
    Class A
    include MyModule(config)
    end
    #=> NoMethodError: undefined method `MyModule' for A:Class

    View Slide

  26. Encapsulated? Nope.
    module MyModule
    def foo
    “foo” + bar
    end
    private
    def bar
    “bar”
    end
    end
    module MyModule
    def foo
    “foo” + bar
    end
    private
    def bar
    “bar”
    end
    end

    View Slide

  27. Encapsulated? Nope.
    module MyModule
    def foo
    “foo” + bar
    end
    private
    def bar
    “bar”
    end
    end
    module MyModule
    def foo
    “foo” + bar
    end
    private
    def bar
    “bar”
    end
    end
    class A
    include MyModule
    def bar
    “baz”
    end
    end
    a = A.new
    a.foo
    #=> “foobaz”
    class A
    include MyModule
    def bar
    “baz”
    end
    end
    a = A.new
    a.foo
    #=> “foobaz”

    View Slide

  28. View Slide

  29. Solution?

    View Slide

  30. Metaprogramming!!!

    View Slide

  31. Building with Modules
    Building with Modules

    View Slide

  32. module Addable
    def +(point)
    self.class.new(point.x + x, point.y + y)
    end
    end
    module Addable
    def +(point)
    self.class.new(point.x + x, point.y + y)
    end
    end
    A Simple Module
    Point = Struct.new(:x, :y)
    Point.include Addable
    p1 = Point.new(1, 2)
    p2 = Point.new(2, 3)
    p1 + p2
    #=> #
    Point = Struct.new(:x, :y)
    Point.include Addable
    p1 = Point.new(1, 2)
    p2 = Point.new(2, 3)
    p1 + p2
    #=> #

    View Slide

  33. class Point < Struct.new(:x, :y)
    include Addable
    def +(point)
    puts “Enter Adder...”
    super.tap { “Exit Adder...” }
    end
    end
    p1 = Point.new(1, 2)
    p2 = Point.new(2, 3)
    p1 + p2
    Enter Adder...
    Exit Adder...
    #=> #
    class Point < Struct.new(:x, :y)
    include Addable
    def +(point)
    puts “Enter Adder...”
    super.tap { “Exit Adder...” }
    end
    end
    p1 = Point.new(1, 2)
    p2 = Point.new(2, 3)
    p1 + p2
    Enter Adder...
    Exit Adder...
    #=> #
    Module Composition

    View Slide

  34. Problem: Abstraction

    Module only defines addition for fixed pair of
    accessors x and y

    Can we abstract our module so that defines
    addition on any set of accessors?

    Can we do it in a way that supports method
    composition?

    View Slide

  35. module AdderDefiner
    def define_adder(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    module AdderDefiner
    def define_adder(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    Bootstrap Methods

    Define a bootstrap method which builds
    instance methods dynamically from arguments

    View Slide

  36. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    Bootstrapping a Method

    View Slide

  37. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    Bootstrapping a Method
    def +(other)
    self.class.new(amount + other.amount,
    tax + other.tax)
    end

    View Slide

  38. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    end
    Bootstrapping a Method
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> #
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> #

    View Slide

  39. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Bootstrapping + Composition

    View Slide

  40. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Bootstrapping + Composition
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> ???
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> ???

    View Slide

  41. NoMethodError
    super: no superclass method `+' for
    #

    View Slide

  42. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Bootstrapping + Composition

    View Slide

  43. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    def +(other)
    self.class.new(amount + other.amount,
    tax + other.tax)
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    def +(other)
    self.class.new(amount + other.amount,
    tax + other.tax)
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Bootstrapping + Composition

    View Slide

  44. class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    def +(other)
    self.class.new(amount + other.amount,
    tax + other.tax)
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderDefiner
    def +(other)
    self.class.new(amount + other.amount,
    tax + other.tax)
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Bootstrapping + Composition

    View Slide

  45. Configurable & Composable

    View Slide

  46. Configurable & Composable
    Define methods directly on extending class

    View Slide

  47. Configurable & Composable
    Define methods directly on extending class
    Define methods on module
    included into extending class

    View Slide

  48. module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    Module Boostrapper

    View Slide

  49. module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    Module Boostrapper
    AdderDefiner#define_adder

    View Slide

  50. module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    module AdderIncluder
    def define_adder(*keys)
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    end
    end
    Module Boostrapper
    Anonymous module

    View Slide

  51. class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Module Composition

    View Slide

  52. class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Module Composition

    View Slide

  53. class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    adder = Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    include adder
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Module Composition
    LineItem.ancestors
    #=> [LineItem,
    #,
    ...

    View Slide

  54. class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    extend AdderIncluder
    define_adder(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    Now it Works
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    Enter Adder...
    Exit Adder...
    #=> #
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    Enter Adder...
    Exit Adder...
    #=> #

    View Slide

  55. In the Wild

    Technique of building anonymous modules to
    include in classes is widely used (e.g. Rails)

    View Slide

  56. Recap
    Addable
    Defines + method on fixed variables x and y.
    AdderDefiner
    Adds class method define_adder which defines + method
    on configurable set of variables.
    AdderIncluder
    Also adds class method define_adder which defines +
    method, but in a composable way using anonymous module.

    View Slide

  57. Is this Modular?

    Boostrap method(s) extended as class methods

    Instance methods defined on anonymous module

    Configuration state stored on including class,
    referenced by methods on anonymous module
    define_foo
    define_foo
    extend
    def foo(*args)
    self.class.foo
    end
    def foo(*args)
    self.class.foo
    end
    include
    MyClass
    @foo
    Module.new

    View Slide

  58. The Module Builder
    The Module Builder

    View Slide

  59. Classes, Modules & Objects

    View Slide

  60. Classes, Modules & Objects

    View Slide

  61. class Module
    Object Model Revisited

    Module is a Class
    is a

    View Slide

  62. class ModuleBuilder < Module
    Object Model Revisited

    Module is a Class

    A class can have subclasses

    View Slide

  63. class ModuleBuilder < Module
    def initialize(method_name)
    Object Model Revisited

    Module is a Class

    A class can have subclasses

    (Sub)classes can define methods

    View Slide

  64. class ModuleBuilder < Module
    def initialize(method_name)
    define_method method_name do
    Object Model Revisited

    Module is a Class

    A class can have subclasses

    (Sub)classes can define methods

    Modules can also define methods

    View Slide

  65. Elements of a Module Builder
    class Attributes < Module
    def initialize(*attribute_names)
    @names = attribute_names
    # ...
    # ...
    end
    def included(klass)
    @names.each do |name|
    define_method name do
    # ...
    end
    # ...
    end
    end
    end
    class Post
    include Attributes.new(:title,
    :content)
    end
    class Attributes < Module
    def initialize(*attribute_names)
    @names = attribute_names
    # ...
    # ...
    end
    def included(klass)
    @names.each do |name|
    define_method name do
    # ...
    end
    # ...
    end
    end
    end
    class Post
    include Attributes.new(:title,
    :content)
    end
    Subclass Module
    Define module methods in
    initialize or included hook
    Include instance of Module
    subclass in other classes
    Configure in initializer

    View Slide

  66. Configure in an Initializer
    class Attributes < Module
    def initialize(*attribute_names)
    @names = attribute_names
    # ...
    end
    # ...
    end
    class Post
    include Attributes.new(:title, :content)
    end
    class Attributes < Module
    def initialize(*attribute_names)
    @names = attribute_names
    # ...
    end
    # ...
    end
    class Post
    include Attributes.new(:title, :content)
    end

    View Slide

  67. Encapsulate in a Closure
    class Attributes < Module
    # ...
    def included(klass)
    @names.each do |name|
    define_method name do
    locale = ...
    mobility_backend_for(name).read(locale)
    end
    end
    end
    end
    class Attributes < Module
    # ...
    def included(klass)
    @names.each do |name|
    define_method name do
    locale = ...
    mobility_backend_for(name).read(locale)
    end
    end
    end
    end

    View Slide

  68. class AdderBuilder < Module
    def initialize(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    class AdderBuilder < Module
    def initialize(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    Addable as Module Builder

    View Slide

  69. class AdderBuilder < Module
    def initialize(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    class AdderBuilder < Module
    def initialize(*keys)
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    end
    Addable as Module Builder
    AdderDefiner#define_adder

    View Slide

  70. Encapsulated Module Building
    class LineItem < Struct.new(:amount, :tax)
    include AdderBuilder.new(:amount, :tax)
    end
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> #
    class LineItem < Struct.new(:amount, :tax)
    include AdderBuilder.new(:amount, :tax)
    end
    l1 = LineItem.new(9.99, 1.50)
    l2 = LineItem.new(15.99, 2.40)
    l1 + l2
    #=> #

    No bootstrap method added to class

    No private methods mixed into class

    View Slide

  71. Composition Too
    class LineItem < Struct.new(:amount, :tax)
    include (Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    include (Module.new do
    define_method :+ do |other|
    self.class.new(
    *(keys.map { |key| send(key) + other.send(key) })
    )
    end
    end
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end

    View Slide

  72. Composition Too
    class LineItem < Struct.new(:amount, :tax)
    include AdderBuilder.new(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    class LineItem < Struct.new(:amount, :tax)
    include AdderBuilder.new(:amount, :tax)
    def +(other)
    puts "Enter Adder..."
    super.tap { puts "Exit Adder..." }
    end
    end
    LineItem.ancestors
    [LineItem,
    #,
    ... ]
    It has a name!

    View Slide

  73. Applying the Pattern
    Applying the Pattern

    View Slide

  74. The Module Builder Pattern
    broader abstraction
    more expressive
    specification
    tighter
    encapsulation

    View Slide

  75. Applying the Pattern
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    BEFORE

    View Slide

  76. Applying the Pattern
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    module Alchemist
    class ModuleBuilder < Module
    def initialize category
    define_inspect_method(category)
    define_unit_methods(category)
    end
    def define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    private
    def define_inspect_method(category)
    define_method :inspect do
    "#"
    end
    end
    def define_unit_methods(category)
    unit_names(category).map do |name|
    define_method name do
    Alchemist.measure self, name.to_sym
    end
    prefixes_for(name).map do |prefix|
    define_method "#{prefix}#{name}" do
    Alchemist.measure_prefixed self, prefix.to_sym, name.to_sym
    end
    end
    end
    end
    def library
    Alchemist.library
    end
    def unit_names(category)
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    end
    end
    module Alchemist
    class ModuleBuilder < Module
    def initialize category
    define_inspect_method(category)
    define_unit_methods(category)
    end
    def define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    private
    def define_inspect_method(category)
    define_method :inspect do
    "#"
    end
    end
    def define_unit_methods(category)
    unit_names(category).map do |name|
    define_method name do
    Alchemist.measure self, name.to_sym
    end
    prefixes_for(name).map do |prefix|
    define_method "#{prefix}#{name}" do
    Alchemist.measure_prefixed self, prefix.to_sym, name.to_sym
    end
    end
    end
    end
    def library
    Alchemist.library
    end
    def unit_names(category)
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    end
    end
    BEFORE
    AFTER

    View Slide

  77. Applying the Pattern
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    module Alchemist
    class ModuleBuilder
    def initialize category
    @category = category
    end
    def build
    build_module do |category_module|
    define_inspect_method(category_module)
    define_unit_methods(category_module)
    end
    end
    private
    attr_reader :category
    def build_module(&block)
    Module.new do
    def self.define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    end.tap &block
    end
    def define_inspect_method(category_module)
    category_module.class_eval %(def self.inspect() "#" end)
    end
    def define_unit_methods(category_module)
    category_module.class_eval category_methods
    end
    def library
    Alchemist.library
    end
    def category_methods
    unit_names.map do |name|
    %(define_method("#{name}") { Alchemist.measure self, :#{name} }) + "\n" +
    prefixed_methods(name)
    end.join("\n")
    end
    def unit_names
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    def prefixed_methods(name)
    prefixes_for(name).map do |prefix|
    %(define_method("#{prefix}#{name}") { Alchemist.measure_prefixed self,
    :#{prefix}, :#{name} })
    end.join("\n")
    end
    end
    end
    module Alchemist
    class ModuleBuilder < Module
    def initialize category
    define_inspect_method(category)
    define_unit_methods(category)
    end
    def define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    private
    def define_inspect_method(category)
    define_method :inspect do
    "#"
    end
    end
    def define_unit_methods(category)
    unit_names(category).map do |name|
    define_method name do
    Alchemist.measure self, name.to_sym
    end
    prefixes_for(name).map do |prefix|
    define_method "#{prefix}#{name}" do
    Alchemist.measure_prefixed self, prefix.to_sym, name.to_sym
    end
    end
    end
    end
    def library
    Alchemist.library
    end
    def unit_names(category)
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    end
    end
    module Alchemist
    class ModuleBuilder < Module
    def initialize category
    define_inspect_method(category)
    define_unit_methods(category)
    end
    def define_unit_method(names)
    names.each do |name|
    define_method(name.to_sym) { Alchemist.measure self, name.to_sym }
    end
    end
    private
    def define_inspect_method(category)
    define_method :inspect do
    "#"
    end
    end
    def define_unit_methods(category)
    unit_names(category).map do |name|
    define_method name do
    Alchemist.measure self, name.to_sym
    end
    prefixes_for(name).map do |prefix|
    define_method "#{prefix}#{name}" do
    Alchemist.measure_prefixed self, prefix.to_sym, name.to_sym
    end
    end
    end
    end
    def library
    Alchemist.library
    end
    def unit_names(category)
    library.unit_names(category)
    end
    def prefixes_for(name)
    if library.si_units.include?(name.to_s)
    library.unit_prefixes.keys
    else
    []
    end
    end
    end
    end
    BEFORE
    AFTER

    View Slide

  78. Compact and Versatile
    class GeoLocation
    include Dry::Equalizer.new(:latitude, :longitude)
    attr_reader :latitude, :longitude
    def initialize(latitude, longitude)
    @latitude, @longitude = latitude, longitude
    end
    end
    class GeoLocation
    include Dry::Equalizer.new(:latitude, :longitude)
    attr_reader :latitude, :longitude
    def initialize(latitude, longitude)
    @latitude, @longitude = latitude, longitude
    end
    end
    def initialize(*keys)
    @keys = keys
    define_methods
    freeze
    end
    def initialize(*keys)
    @keys = keys
    define_methods
    freeze
    end
    def define_methods
    define_cmp_method
    define_hash_method
    define_inspect_method
    end
    def define_methods
    define_cmp_method
    define_hash_method
    define_inspect_method
    end

    View Slide

  79. Compact and Versatile
    point_a = GeoLocation.new(1, 2)
    point_b = GeoLocation.new(1, 2)
    point_c = GeoLocation.new(2, 2)
    point_a.inspect
    #=> "#"
    point_a == point_b # => true
    point_a.hash == point_b.hash # => true
    point_a.eql?(point_b) # => true
    point_a.equal?(point_b) # => false
    point_a == point_c # => false
    point_a.hash == point_c.hash # => false
    point_a.eql?(point_c) # => false
    point_a.equal?(point_c) # => false
    point_a = GeoLocation.new(1, 2)
    point_b = GeoLocation.new(1, 2)
    point_c = GeoLocation.new(2, 2)
    point_a.inspect
    #=> "#"
    point_a == point_b # => true
    point_a.hash == point_b.hash # => true
    point_a.eql?(point_b) # => true
    point_a.equal?(point_b) # => false
    point_a == point_c # => false
    point_a.hash == point_c.hash # => false
    point_a.eql?(point_c) # => false
    point_a.equal?(point_c) # => false

    View Slide

  80. Flexible and Extendable
    Title Backend
    #
    #
    read(:fr)
    Storage
    Strategy
    ...
    class Post
    extend Mobility
    translates :title,
    fallbacks: { fr: :en },
    default: ‘No Translation’
    end
    I18n.locale = :fr
    post = Post.new
    post.title
    #=> post.title_backend.read(:fr)
    class Post
    extend Mobility
    translates :title,
    fallbacks: { fr: :en },
    default: ‘No Translation’
    end
    I18n.locale = :fr
    post = Post.new
    post.title
    #=> post.title_backend.read(:fr)

    Mobility uses Module Builders to:
    – Customize translation accessors per-attribute
    – Add attribute-specific customization to models

    View Slide

  81. But umm, is this... okay?

    View Slide

  82. Don’t be Afraid!

    A Module Builder is:
    – Just a subclass
    – Just configuring an
    instance
    – Just defining methods
    on a module

    View Slide

  83. But most of all, a Module Builder is just...

    View Slide

  84. But most of all, a Module Builder is just...
    PLAIN OLD RUBY
    RUBY

    View Slide

  85. Thanks!
    Thanks!

    View Slide