Middleware: A General Purpose Abstraction

Middleware: A General Purpose Abstraction

2828f28fb012308a7786eee83b8293c5?s=128

Mitchell Hashimoto

June 02, 2012
Tweet

Transcript

  1. Middleware A general abstraction.

  2. Mitchell Hashimoto @mitchellh

  3. None
  4. None
  5. None
  6. Large Classes

  7. Large Classes

  8. Large Classes

  9. Large Classes

  10. Large Classes

  11. Large Classes

  12. Problems:

  13. 1. Unapproachable

  14. 2. Unclear dependencies

  15. 3. Difficult to Test

  16. Mixin all the things!

  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
  18. Function Composition F*** Yeah.

  19. 1 def f(value) 2 value + 1 3 end 4

    5 def g(value) 6 value * 2 7 end
  20. 1 f(g(1)) # => 3 2 3 g(f(1)) # =>

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

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

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

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

    4 True Separation of Logic
  25. Function Composition Applied to Large Classes?

  26. Middleware

  27. Rack Middleware

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

  29. Request Processing

  30. 1 app = Rack::Builder.new do 2 use Middleware::Example 3 use

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

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

    Rack::CommonLogger 4 use Rack::ShowExceptions 5 end
  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
  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
  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
  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
  37. 1 app = Rack::Builder.new do 2 use Middleware::Example 3 use

    Rack::CommonLogger 4 use Rack::ShowExceptions 5 end
  38. Why is middleware so amazing?

  39. Explicit Ordering

  40. 1 app = Rack::Builder.new do 2 use Middleware::Example 3 use

    Rack::CommonLogger 4 use Rack::ShowExceptions 5 end
  41. Visible Dependencies

  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
  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
  44. Easily Tested

  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
  46. 1 state = { "path" => "/foo" } 2 m

    = Middleware::Example.new 3 m.call(state) 4 assert state["path"] == "/bar"
  47. Extensible: Subclass!

  48. Vagrant vagrantup.com

  49. Virtual Machine Lifecycle

  50. $ vagrant suspend

  51. 1 Builder.new do 2 use General::CheckVirtualbox 3 use General::Validate 4

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

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

    use VM::CheckAccessible 5 use VM::Suspend 6 end
  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
  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
  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
  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
  58. 1 Builder.new do 2 use General::CheckVirtualbox 3 use General::Validate 4

    use VM::CheckAccessible 5 use VM::Suspend 6 end
  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
  60. $ vagrant up

  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
  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
  63. Scared?

  64. Real Example

  65. Diaspora user.rb

  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  76. 1 Builder.new do 2 use HaltIfInvited 3 use SetupUser 4

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

  78. When and Where

  79. Reuse

  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
  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
  82. Serial Invocations

  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
  84. Composition

  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)
  86. Mitchell Hashimoto @mitchellh THANKS!