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 Slide

  2. Mitchell Hashimoto
    @mitchellh

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. Large Classes

    View Slide

  7. Large Classes

    View Slide

  8. Large Classes

    View Slide

  9. Large Classes

    View Slide

  10. Large Classes

    View Slide

  11. Large Classes

    View Slide

  12. Problems:

    View Slide

  13. 1. Unapproachable

    View Slide

  14. 2. Unclear dependencies

    View Slide

  15. 3. Difficult to Test

    View Slide

  16. Mixin all the things!

    View Slide

  17. 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 Slide

  18. Function Composition
    F*** Yeah.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  25. Function Composition
    Applied to Large Classes?

    View Slide

  26. Middleware

    View Slide

  27. Rack Middleware

    View Slide

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

    View Slide

  29. Request Processing

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. 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 Slide

  34. 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 Slide

  35. 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 Slide

  36. 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 Slide

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

    View Slide

  38. Why is middleware
    so amazing?

    View Slide

  39. Explicit Ordering

    View Slide

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

    View Slide

  41. Visible Dependencies

    View Slide

  42. 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 Slide

  43. 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 Slide

  44. Easily Tested

    View Slide

  45. 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 Slide

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

    View Slide

  47. Extensible: Subclass!

    View Slide

  48. Vagrant
    vagrantup.com

    View Slide

  49. Virtual Machine Lifecycle

    View Slide

  50. $ vagrant suspend

    View Slide

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

    View Slide

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

    View 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 Slide

  54. 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 Slide

  55. 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 Slide

  56. 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 Slide

  57. 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 Slide

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

    View Slide

  59. 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 Slide

  60. $ vagrant up

    View Slide

  61. 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 Slide

  62. 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 Slide

  63. Scared?

    View Slide

  64. Real Example

    View Slide

  65. Diaspora user.rb

    View Slide

  66. 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 Slide

  67. 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 Slide

  68. 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 Slide

  69. 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 Slide

  70. 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 Slide

  71. 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 Slide

  72. 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 Slide

  73. 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 Slide

  74. 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 Slide

  75. 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 Slide

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

    View Slide

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

    View Slide

  78. When and Where

    View Slide

  79. Reuse

    View Slide

  80. 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 Slide

  81. 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 Slide

  82. Serial Invocations

    View Slide

  83. 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 Slide

  84. Composition

    View Slide

  85. 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 Slide

  86. Mitchell Hashimoto
    @mitchellh
    THANKS!

    View Slide