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

Middleware: A General Purpose Abstraction

Middleware: A General Purpose Abstraction

Mitchell Hashimoto

June 02, 2012
Tweet

More Decks by Mitchell Hashimoto

Other Decks in Programming

Transcript

  1. Middleware
    A general abstraction.

    View full-size slide

  2. Mitchell Hashimoto
    @mitchellh

    View full-size slide

  3. Large Classes

    View full-size slide

  4. Large Classes

    View full-size slide

  5. Large Classes

    View full-size slide

  6. Large Classes

    View full-size slide

  7. Large Classes

    View full-size slide

  8. Large Classes

    View full-size slide

  9. 1. Unapproachable

    View full-size slide

  10. 2. Unclear dependencies

    View full-size slide

  11. 3. Difficult to Test

    View full-size slide

  12. Mixin all the things!

    View full-size slide

  13. 1 module Authlogic
    2 module Session # :nodoc:
    3 # This is the base class Authlogic, where all modules are included.
    4 #
    5 class Base
    6 include Foundation
    7 include Callbacks
    8
    9 # Included first so that the session resets itself to nil
    10 include Timeout
    11
    12 # Included in a specific order so they are tried in this order when persisting
    13 include Params
    14 include Cookies
    15 include Session
    16 include HttpAuth
    17
    18 # Included in a specific order so magic states gets ran after a record is found
    19 include Password
    20 include UnauthorizedRecord
    21 include MagicStates
    22
    23 include Activation
    24 include ActiveRecordTrickery
    25 include BruteForceProtection
    26 include Existence
    27 include Klass
    28 include MagicColumns
    29 include PerishableToken
    30 include Persistence
    31 include Scopes
    32 include Id
    33 include Validation
    34 include PriorityRecord
    35 end
    36 end
    37 end

    View full-size slide

  14. Function Composition
    F*** Yeah.

    View full-size slide

  15. 1 def f(value)
    2 value + 1
    3 end
    4
    5 def g(value)
    6 value * 2
    7 end

    View full-size slide

  16. 1 f(g(1)) # => 3
    2
    3 g(f(1)) # => 4

    View full-size slide

  17. 1 f(g(1)) # => 3
    2
    3 g(f(1)) # => 4
    Clear Ordering

    View full-size slide

  18. 1 f(g(1)) # => 3
    2
    3 g(f(1)) # => 4
    Clear Dependencies

    View full-size slide

  19. 1 f(g(1)) # => 3
    2
    3 g(f(1)) # => 4
    Easy to Test

    View full-size slide

  20. 1 f(g(1)) # => 3
    2
    3 g(f(1)) # => 4
    True Separation of Logic

    View full-size slide

  21. Function Composition
    Applied to Large Classes?

    View full-size slide

  22. Rack Middleware

    View full-size slide

  23. Python Web Server Gateway
    Interface v1.0
    python.org/dev/peps/pep-0333/

    View full-size slide

  24. Request Processing

    View full-size slide

  25. 1 app = Rack::Builder.new do
    2 use Middleware::Example
    3 use Rack::CommonLogger
    4 use Rack::ShowExceptions
    5 end

    View full-size slide

  26. 1 app = Rack::Builder.new do
    2 use Middleware::Example
    3 use Rack::CommonLogger
    4 use Rack::ShowExceptions
    5 end

    View full-size slide

  27. 1 app = Rack::Builder.new do
    2 use Middleware::Example
    3 use Rack::CommonLogger
    4 use Rack::ShowExceptions
    5 end

    View full-size slide

  28. 1 module Middleware
    2 class Example
    3 def initialize(app)
    4 @app = app
    5 end
    6
    7 def call(env)
    8 # do something before the next middleware
    9 # possibly modify the environment
    10
    11 # run the next middleware in the stack
    12 @app.call(env)
    13
    14 # do something after the next middleware
    15 end
    16 end
    17 end

    View full-size slide

  29. 1 module Middleware
    2 class Example
    3 def initialize(app)
    4 @app = app
    5 end
    6
    7 def call(env)
    8 # do something before the next middleware
    9 # possibly modify the environment
    10
    11 # run the next middleware in the stack
    12 @app.call(env)
    13
    14 # do something after the next middleware
    15 end
    16 end
    17 end

    View full-size slide

  30. 1 module Middleware
    2 class Example
    3 def initialize(app)
    4 @app = app
    5 end
    6
    7 def call(env)
    8 # do something before the next middleware
    9 # possibly modify the environment
    10
    11 # run the next middleware in the stack
    12 @app.call(env)
    13
    14 # do something after the next middleware
    15 end
    16 end
    17 end

    View full-size slide

  31. 1 module Middleware
    2 class Example
    3 def initialize(app)
    4 @app = app
    5 end
    6
    7 def call(env)
    8 # do something before the next middleware
    9 # possibly modify the environment
    10
    11 # run the next middleware in the stack
    12 @app.call(env)
    13
    14 # do something after the next middleware
    15 end
    16 end
    17 end

    View full-size slide

  32. 1 app = Rack::Builder.new do
    2 use Middleware::Example
    3 use Rack::CommonLogger
    4 use Rack::ShowExceptions
    5 end

    View full-size slide

  33. Why is middleware
    so amazing?

    View full-size slide

  34. Explicit Ordering

    View full-size slide

  35. 1 app = Rack::Builder.new do
    2 use Middleware::Example
    3 use Rack::CommonLogger
    4 use Rack::ShowExceptions
    5 end

    View full-size slide

  36. Visible Dependencies

    View full-size slide

  37. 1 module Middleware
    2 class Example
    3 # ...
    4
    5 def call(env)
    6 if env["path"] == "/foo"
    7 env["path"] = "/bar"
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  38. 1 module Middleware
    2 class Example
    3 # ...
    4
    5 def call(env)
    6 if env["path"] == "/foo"
    7 env["path"] = "/bar"
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  39. Easily Tested

    View full-size slide

  40. 1 module Middleware
    2 class Example
    3 # ...
    4
    5 def call(env)
    6 if env["path"] == "/foo"
    7 env["path"] = "/bar"
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  41. 1 state = { "path" => "/foo" }
    2 m = Middleware::Example.new
    3 m.call(state)
    4 assert state["path"] == "/bar"

    View full-size slide

  42. Extensible: Subclass!

    View full-size slide

  43. Vagrant
    vagrantup.com

    View full-size slide

  44. Virtual Machine Lifecycle

    View full-size slide

  45. $ vagrant suspend

    View full-size slide

  46. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::Suspend
    6 end

    View full-size slide

  47. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::Suspend
    6 end

    View full-size slide

  48. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::Suspend
    6 end

    View full-size slide

  49. 1 module Vagrant::Action::General
    2 class Validate
    3 # ...
    4
    5 def call(env)
    6 if env[:validate]
    7 env[:vm].config.validate!(env[:vm].env)
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  50. 1 module Vagrant::Action::General
    2 class Validate
    3 # ...
    4
    5 def call(env)
    6 if env[:validate]
    7 env[:vm].config.validate!(env[:vm].env)
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  51. 1 module Vagrant::Action::General
    2 class Validate
    3 # ...
    4
    5 def call(env)
    6 if env[:validate]
    7 env[:vm].config.validate!(env[:vm].env)
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  52. 1 module Vagrant::Action::General
    2 class Validate
    3 # ...
    4
    5 def call(env)
    6 if env[:validate]
    7 env[:vm].config.validate!(env[:vm].env)
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  53. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::Suspend
    6 end

    View full-size slide

  54. 1 module Vagrant::Action::VM
    2 class Suspend
    3 # ...
    4
    5 def call(env)
    6 if env[:vm].state == :running
    7 env[:vm].driver.suspend
    8 end
    9
    10 @app.call(env)
    11 end
    12 end
    13 end

    View full-size slide

  55. $ vagrant up

    View full-size slide

  56. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::CheckBox
    6 use VM::Import
    7 use VM::CheckGuestAdditions
    8 use VM::DefaultName
    9 use VM::MatchMACAddress
    10 use VM::CleanMachineFolder
    11 use VM::ClearForwardedPorts
    12 use VM::CheckPortCollisions
    13 use VM::ForwardPorts
    14 use VM::Provision
    15 use VM::PruneNFSExports
    16 use VM::NFS
    17 use VM::ClearSharedFolders
    18 use VM::ShareFolders
    19 use VM::HostName
    20 use VM::ClearNetworkInterfaces
    21 use VM::Network
    22 use VM::Customize
    23 use VM::Boot
    24 end

    View full-size slide

  57. 1 Builder.new do
    2 use General::CheckVirtualbox
    3 use General::Validate
    4 use VM::CheckAccessible
    5 use VM::CheckBox
    6 use VM::Import
    7 use VM::CheckGuestAdditions
    8 use VM::DefaultName
    9 use VM::MatchMACAddress
    10 use VM::CleanMachineFolder
    11 use VM::ClearForwardedPorts
    12 use VM::CheckPortCollisions
    13 use VM::ForwardPorts
    14 use VM::Provision
    15 use VM::PruneNFSExports
    16 use VM::NFS
    17 use VM::ClearSharedFolders
    18 use VM::ShareFolders
    19 use VM::HostName
    20 use VM::ClearNetworkInterfaces
    21 use VM::Network
    22 use VM::Customize
    23 use VM::Boot
    24 end

    View full-size slide

  58. Real Example

    View full-size slide

  59. Diaspora user.rb

    View full-size slide

  60. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  61. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  62. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  63. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  64. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  65. 1 def accept_invitation!(opts = {})
    2 if self.invited?
    3 self.setup(opts)
    4 self.invitation_token = nil
    5 self.password = opts[:password]
    6 self.password_confirmation = opts[:password_confirmation]
    7
    8 self.save
    9 return unless self.errors.empty?
    10
    11 # moved old Invitation#share_with! logic into here,
    12 # but i don't think we want to destroy the invitation
    13 # anymore. we may want to just call self.share_with
    14 invitations_to_me.each do |invitation|
    15 if !invitation.admin? &&
    invitation.sender.share_with(self.person,
    invitation.aspect)
    16 invitation.destroy
    17 end
    18 end
    19
    20 self
    21 end
    22 end

    View full-size slide

  66. 1 def setup(opts)
    2 self.username = opts[:username]
    3 self.email = opts[:email]
    4 self.language = opts[:language]
    5 self.language ||= I18n.locale.to_s
    6 self.valid?
    7 errors = self.errors
    8 errors.delete :person
    9 return if errors.size > 0
    10 self.set_person(Person.new(opts[:person] || {} ))
    11 self.generate_keys
    12 self
    13 end

    View full-size slide

  67. 1 class SetupUser
    2 def call(env)
    3 user = env[:user]
    4
    5 user.username = env[:username]
    6 user.email = env[:email]
    7 user.language = env[:language]
    8 user.language ||= I18n.locale.to_s
    9 user.valid?
    10 errors = user.errors
    11 errors.delete :person
    12 raise ErrorsHappened if errors.size > 0
    13 user.set_person(Person.new(env[:person] || {} ))
    14 user.generate_keys
    15 end
    16 end

    View full-size slide

  68. 1 class SetupUser
    2 def call(env)
    3 user = env[:user]
    4
    5 user.username = env[:username]
    6 user.email = env[:email]
    7 user.language = env[:language]
    8 user.language ||= I18n.locale.to_s
    9 user.valid?
    10 errors = user.errors
    11 errors.delete :person
    12 raise ErrorsHappened if errors.size > 0
    13 user.set_person(Person.new(env[:person] || {} ))
    14 user.generate_keys
    15 @app.call(env)
    16 end
    17 end

    View full-size slide

  69. 1 class SetupUser
    2 def call(env)
    3 user = env[:user]
    4
    5 user.username = env[:username]
    6 user.email = env[:email]
    7 user.language = env[:language]
    8 user.language ||= I18n.locale.to_s
    9 user.valid?
    10 errors = user.errors
    11 errors.delete :person
    12 raise ErrorsHappened if errors.size > 0
    13 user.set_person(Person.new(env[:person] || {} ))
    14 user.generate_keys
    15 @app.call(env)
    16 end
    17 end

    View full-size slide

  70. 1 Builder.new do
    2 use HaltIfInvited
    3 use SetupUser
    4 use SetPassword
    5 use ClearInvitations
    6 end

    View full-size slide

  71. 1 def accept_invitation!(opts={})
    2 run(:accept_invitation, opts.merge(:user => self))
    3 end

    View full-size slide

  72. When and Where

    View full-size slide

  73. 1 Builder.new do
    2 use SetupUser
    3 use AcceptInvite
    4 use ConfirmationEmail
    5 end
    6
    7 Builder.new do
    8 use SetupUser
    9 use Signup
    10 end

    View full-size slide

  74. 1 Builder.new do
    2 use SetupUser
    3 use AcceptInvite
    4 use ConfirmationEmail
    5 end
    6
    7 Builder.new do
    8 use SetupUser
    9 use Signup
    10 end

    View full-size slide

  75. Serial Invocations

    View full-size slide

  76. 1 def do_something!
    2 foo(1)
    3 bar(2)
    4 baz(3)
    5 end
    6
    7 Builder.new do
    8 use Foo, 1
    9 use Bar, 2
    10 use Baz, 3
    11 end

    View full-size slide

  77. 1 def do_something!
    2 foo(bar(baz(1)))
    3 end
    4
    5 Builder.new {
    6 use Baz
    7 use Bar
    8 use Foo
    9 }.call(1)

    View full-size slide

  78. Mitchell Hashimoto
    @mitchellh
    THANKS!

    View full-size slide