Subclassing modules
Render problem
Trying to determine all dependencies for #render
module ActionView
module Helpers
module RenderingHelper
def render(options = {}, locals = {}, &block)
# ...
view_renderer.render_partial
end
end
end
end
module ActionView
module Rendering
# ...
def view_renderer
ActionView::Renderer.new(lookup_context)
end
end
end
ActionController::Base#render
ActionController::Base
includes AbstractController::Rendering
implements #render
calls #render_to_body
includes ActionView::Layouts
includes ActionView::Rendering
implements #render_to_body
calls #view_renderer
implements #view_renderer
calls #lookup_context
includes ActionView::ViewPaths
implements #lookup_context
ResultsController.ancestors
=> [ApplicationController,
# ... ~ 60 ancestors
ActionController::Metal,
AbstractController::Base,
ActiveSupport::Configurable,
Object,
PP::ObjectMixin,
FriendlyId::ObjectUtils,
ActiveSupport::Dependencies::Loadable,
JSON::Ext::Generator::GeneratorMethods::Object,
Kernel,
BasicObject]
What's the problem?
- modules are designed to dry-up closely related group of method used in multiple places
- they are usable beyond that, but as complexity grows call stack jumps all around source code
- module can't specify it's dependencies, so it calls ghost methods
- ghost implementations can be in host(simple case), inherited(reasonably simple) or can be included from another modules
module RenderingHelper
# Implements methods that allow rendering from a view context.
# In order to use this module, all you need is to implement
# view_renderer that returns an ActionView::Renderer object.
def render(options = {}, locals = {}, &block)
view_renderer.render_parital
end
end
Includable?
class Hero
end
module Wingman
end
class Guest < Module
end
class Host
include Hero # ?
include Hero.new # ?
include Wingman # ?
include Wingman.new # ?
include Guest # ?
include Guest.new # ?
end
Host#include(guest)
- guest can't be a class
- guest must be kind of a module
- checks are performed in short-circuit mode
Quirks mode: ON
-
include is a three step process
- prepend guest to ancestor chain
- evaluate guest
- run hook on guest
- evaluation doesn't change context(quirk)
- Host is active class in guest constructor
class Drivable < Module
def initialize(vehicle)
define_method(:drive) { vehicle }
end
def transmission
:manual
end
end
class Hero
include Drivable.new(:batmobil)
end
Hero.new.public_methods.grep(/transmission|drive/) # => ???
Technique skeleton
class Guest < Module
attr_reader :state
# 2. store private state
def initialize(state)
@state = state
create_methods
end
# 4. hook behavior into host class
def included(host)
# host.class_eval { }
# host.send(:include)
end
def create_methods
ctx = self
# 3. evaluate code in context of host class
define_method :to_s { ctx.state[:name] }
end
end
class Host
# 1. configure Guest dependencies/state
include Guest.new(name: 'me',
renderer: Renderer.new)
end
- Host can provide dependencies through constructor
- Guest instance has a private state completely inaccessible to host class/instance(due to method lookup)
- utilize quirks mode, initiate method definitions from constructor
- extend host class from hook method(quirk does not work here anymore)
Under the hood
- each inheritance prepends parent class to ancestor chain
- each include prepends guest to ancestor chain
- each extend prepends guest to singletons class ancestor chain
- when resolving methods ruby interpreter consults ancestors chain
- classes and modules are searched in order of appearance
- objects are skipped
Why are guest instance methods invisible?
I'm the last slide
Questions?
Subclassing modules
Subclassing modules
By vrabac
Subclassing modules
- 525