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

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. 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
  2. “A self-contained component of a system, often interchangeable, which has

    a well- defined interface to the other components.”
  3. Modular design is an attempt to combine the advantages of

    standardization with those of customization. - Wikipedia Modular Design
  4. Elements of Module Design • Abstraction Extract common pattern into

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

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

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

    storage strategies into “backends” • Supports a flexible collection of plugins Plugins Backend
  10. 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”
  11. 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
  12. 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
  13. 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”
  14. 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>
  15. 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
  16. 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?
  17. 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
  18. 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
  19. 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
  20. 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>
  21. 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
  22. 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 #=> ???
  23. 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
  24. 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
  25. 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
  26. Configurable & Composable Define methods directly on extending class Define

    methods on module included into extending class
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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...>, ...
  33. 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>
  34. In the Wild • Technique of building anonymous modules to

    include in classes is widely used (e.g. Rails)
  35. 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.
  36. 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
  37. class ModuleBuilder < Module def initialize(method_name) Object Model Revisited •

    Module is a Class • A class can have subclasses • (Sub)classes can define methods
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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!
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. Don’t be Afraid! • A Module Builder is: – Just

    a subclass – Just configuring an instance – Just defining methods on a module