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
Elements of Module Design ● Abstraction Extract common pattern into functional unit ● Specification Define contractual interface for unit ● Encapsulation Hide implementation behind public interface
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 mobility
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”
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
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
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”
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?
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
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
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
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
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
class LineItem < Struct.new(:amount, :tax) extend AdderIncluder adder = Module.new do define_method :+ do |other| self.class.new(amount + other.amount, tax + other.tax) 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(amount + other.amount, tax + other.tax) end end include adder def +(other) puts "Enter Adder..." super.tap { puts "Exit Adder..." } end end Module Composition
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.
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
class ModuleBuilder < Module def initialize(method_name) Object Model Revisited ● Module is a Class ● A class can have subclasses ● (Sub)classes can define methods
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
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
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
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
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
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
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
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
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!
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
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
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
Really Useful class Intrumentation < Module def initialize(method) @method = method define_method(method) do |*args, &block| value = nil self.class.trace_execution_scoped([@method]) do value = super(*args, &block) end value end end def prepended(klass) klass.extend ::NewRelic::Agent::MethodTracer unless klass.is_a?(::NewRelic::Agent::MethodTracer) end end Sleeper.prepend Instrumentation.new(:sleep) Sleeper.prepend Instrumentation.new(:deep_sleep) class Intrumentation < Module def initialize(method) @method = method define_method(method) do |*args, &block| value = nil self.class.trace_execution_scoped([@method]) do value = super(*args, &block) end value end end def prepended(klass) klass.extend ::NewRelic::Agent::MethodTracer unless klass.is_a?(::NewRelic::Agent::MethodTracer) end end Sleeper.prepend Instrumentation.new(:sleep) Sleeper.prepend Instrumentation.new(:deep_sleep)