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

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

September 19, 2017
Tweet

Transcript

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

    Salzberg ^
  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
  3. None
  4. We’re Hiring!

  5. Introduction Introduction

  6. None
  7. MODULE

  8. MODULE What the heck is a anyway?

  9. “A self-contained component of a system, often interchangeable, which has

    a well- defined interface to the other components.”
  10. None
  11. None
  12. None
  13. None
  14. Modular design is an attempt to combine the advantages of

    standardization with those of customization. - Wikipedia Modular Design
  15. Elements of Module Design

  16. Elements of Module Design • Abstraction Extract common pattern into

    functional unit
  17. Elements of Module Design • Abstraction Extract common pattern into

    functional unit • Specification Define contractual interface for unit
  18. Elements of Module Design • Abstraction Extract common pattern into

    functional unit • Specification Define contractual interface for unit • Encapsulation Hide implementation behind public interface
  19. Scaling Rule The broader the scope of your abstraction the

    more expressive your specification must be the tighter your encapsulation must be
  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)
  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
  22. A Modular Gem: Mobility • Pluggable “Translation Framework” • Abstracts

    storage strategies into “backends” • Supports a flexible collection of plugins Plugins Backend
  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”
  24. Flexible? Nope. module MyModule(config) def foo config end end module

    MyModule(config) def foo config end end
  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
  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
  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”
  28. None
  29. Solution?

  30. Metaprogramming!!!

  31. Building with Modules Building with Modules

  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:.. @x=3, @y=5> Point = Struct.new(:x, :y) Point.include Addable p1 = Point.new(1, 2) p2 = Point.new(2, 3) p1 + p2 #=> #<Point:.. @x=3, @y=5>
  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... #=> #<Point:.. @x=3, @y=5> 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... #=> #<Point:.. @x=3, @y=5> Module Composition
  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?
  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
  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
  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
  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 #=> #<LineItem:... @amount=25.98, @tax=3.9> l1 = LineItem.new(9.99, 1.50) l2 = LineItem.new(15.99, 2.40) l1 + l2 #=> #<LineItem:... @amount=25.98, @tax=3.9>
  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
  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 #=> ???
  41. NoMethodError super: no superclass method `+' for #<struct LineItem amount=9.99,

    tax=1.5>
  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
  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
  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
  45. Configurable & Composable

  46. Configurable & Composable Define methods directly on extending class

  47. Configurable & Composable Define methods directly on extending class Define

    methods on module included into extending class
  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
  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
  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
  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
  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
  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, #<Module:0x...>, ...
  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... #=> #<LineItem:... @amount=25.98, @tax=3.9> l1 = LineItem.new(9.99, 1.50) l2 = LineItem.new(15.99, 2.40) l1 + l2 Enter Adder... Exit Adder... #=> #<LineItem:... @amount=25.98, @tax=3.9>
  55. In the Wild • Technique of building anonymous modules to

    include in classes is widely used (e.g. Rails)
  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.
  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
  58. The Module Builder The Module Builder

  59. Classes, Modules & Objects

  60. Classes, Modules & Objects

  61. class Module Object Model Revisited • Module is a Class

    is a
  62. class ModuleBuilder < Module Object Model Revisited • Module is

    a Class • A class can have subclasses
  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
  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
  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
  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
  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
  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
  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
  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 #=> #<LineItem:... @amount=25.98, @tax=3.9> 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 #=> #<LineItem:... @amount=25.98, @tax=3.9> • No bootstrap method added to class • No private methods mixed into class
  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
  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, #<AdderBuilder:0x...>, ... ] It has a name!
  73. Applying the Pattern Applying the Pattern

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

    encapsulation
  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() "#<Module(#{category})>" 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() "#<Module(#{category})>" 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
  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() "#<Module(#{category})>" 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() "#<Module(#{category})>" 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 "#<Module(#{category})>" 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 "#<Module(#{category})>" 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
  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() "#<Module(#{category})>" 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() "#<Module(#{category})>" 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 "#<Module(#{category})>" 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 "#<Module(#{category})>" 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
  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
  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 #=> "#<GeoLocation latitude=1 longitude=2>" 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 #=> "#<GeoLocation latitude=1 longitude=2>" 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
  80. Flexible and Extendable Title Backend #<Fallbacks> #<Default> 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
  81. But umm, is this... okay?

  82. Don’t be Afraid! • A Module Builder is: – Just

    a subclass – Just configuring an instance – Just defining methods on a module
  83. But most of all, a Module Builder is just...

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

    OLD RUBY RUBY
  85. Thanks! Thanks!