Upload
andymaleh
View
799
Download
4
Tags:
Embed Size (px)
Citation preview
Rails Engine PatternsAndy Maleh
Software Engineer for Groupon (Oct 2011 - Oct 2012)
Former Senior Consultant for Obtiva
Problem
Difficulty reusing functionality cutting across:Difficulty reusing functionality cutting across:
ModelsModels
ViewsViews
ControllersControllers
Assets (JS, CSS, Images)Assets (JS, CSS, Images)
Duplication across all web application layers.Duplication across all web application layers.
Courtesy of © 2002-2011 National Collegiate Scouting Association - All Rights ReservedCourtesy of © 2002-2011 National Collegiate Scouting Association - All Rights Reserved
Solution
Break common behavior into Rails EnginesBreak common behavior into Rails Engines
Customize models/controllers/helpers in each Customize models/controllers/helpers in each project where needed by reopening classesproject where needed by reopening classes
Customize Rails views in each project as Customize Rails views in each project as needed by overriding templatesneeded by overriding templates
Link to Rails Engines in Gemfile via Git repoLink to Rails Engines in Gemfile via Git repo
Courtesy of © 2002-2011 National Collegiate Scouting Association - All Rights ReservedCourtesy of © 2002-2011 National Collegiate Scouting Association - All Rights Reserved
Courtesy of © 2002-2011 National Collegiate Scouting Association - All Rights ReservedCourtesy of © 2002-2011 National Collegiate Scouting Association - All Rights Reserved
What is a Rails Engine?
Ruby Gem Ruby Gem
+ +
MVC stack elementsMVC stack elements
What is a Rails Engine?
Rails Engines let applications reuse:Rails Engines let applications reuse:
Models / Views / Controllers / HelpersModels / Views / Controllers / Helpers
Assets (JS, CSS, Images)Assets (JS, CSS, Images)
RoutesRoutes
Rake tasksRake tasks
GeneratorsGenerators
What is a Rails Engine?
Rails Engines let applications reuse:Rails Engines let applications reuse:
InitializersInitializers
RSpec / CucumberRSpec / Cucumber
Rack application with middleware stackRack application with middleware stack
Migrations and seed dataMigrations and seed data
LibrariesLibraries
Engine Definition
An engine structure is similar to a Rails app An engine structure is similar to a Rails app having app, config, lib, spec, features, etc…having app, config, lib, spec, features, etc…
lib/engine_name.rb (engine loader)lib/engine_name.rb (engine loader)
lib/engine_name/engine.rb (engine class)lib/engine_name/engine.rb (engine class)
To reuse engine, generate gemspec (using To reuse engine, generate gemspec (using “Jeweler or Gemcutter”)“Jeweler or Gemcutter”)
lib/engine_name.rb
lib/engine_name/engine.rb
Isolated Engines
To avoid Ruby namespace clash with To avoid Ruby namespace clash with Models/Helpers/Controllers, you can define an Models/Helpers/Controllers, you can define an isolated (namespaced) engine.isolated (namespaced) engine.
Add “isolate_namespace MyEngine” to engine Add “isolate_namespace MyEngine” to engine class bodyclass body
For more details, go to For more details, go to http://edgeapi.rubyonrails.org/classes/Rails/http://edgeapi.rubyonrails.org/classes/Rails/Engine.htmlEngine.html
Engine Consumption
Reference engine via Gemfile as a Ruby gem or Reference engine via Gemfile as a Ruby gem or Git repo hosted gemified project:Git repo hosted gemified project:
Courtesy of © 2002-2011 National Collegiate Scouting Association - All Rights ReservedCourtesy of © 2002-2011 National Collegiate Scouting Association - All Rights Reserved
Engine Consumption
To avoid frequent updates of ref (outsider advice):To avoid frequent updates of ref (outsider advice):
Courtesy of © 2002-2011 National Collegiate Scouting Association - All Rights ReservedCourtesy of © 2002-2011 National Collegiate Scouting Association - All Rights Reserved
Load Order
Typically Rails app files load first before Engine Typically Rails app files load first before Engine files. files.
Strongly recommended to reverse so that Strongly recommended to reverse so that engine’s code is customizable in appengine’s code is customizable in app
Load Order
Reversing load order can happen in one of two Reversing load order can happen in one of two ways:ways:
Patching “active_support/dependencies.rb” in Patching “active_support/dependencies.rb” in Rails 3.1- (see next slide)Rails 3.1- (see next slide)
Adjusting railties_order in Rails 3.2+Adjusting railties_order in Rails 3.2+
Engine Configuration
Engines can be configured to customize rack Engines can be configured to customize rack middleware, load paths, generators, and Rails middleware, load paths, generators, and Rails component paths. More details at: component paths. More details at: httphttp://edgeapi.rubyonrails.org/classes/Rails/://edgeapi.rubyonrails.org/classes/Rails/Engine.htmlEngine.html
View and Asset Customization
Engine View and Asset presentation can be Engine View and Asset presentation can be customized by redefining files in Rails Appcustomized by redefining files in Rails App
Customizations Customizations completely overridecompletely override files in Engine files in Engine
Examples of customizable View and Asset files:Examples of customizable View and Asset files:ERB/HamlERB/Haml
JSJS
CSSCSS
ImagesImages
Ruby Code Customization
Model/Helper/Controller behavior can be Model/Helper/Controller behavior can be customized be redefining .rb files in Rails Appcustomized be redefining .rb files in Rails App
Customizations get Customizations get mixed inmixed in with files in with files in EngineEngine
This allows:This allows:
Adding new methods/behaviorAdding new methods/behavior
Replacing existing methodsReplacing existing methods
Extending existing methods (alias_method_chain)Extending existing methods (alias_method_chain)
Ruby Code Customization Example
Ruby Code Customization Example
View Customization Example
View Customization Example
Recommended Engine Code Management• Each Engine lives in own Repo independent of Each Engine lives in own Repo independent of
Rails AppRails App
• Each Engine has its own Gem dependencies Each Engine has its own Gem dependencies (Gemfile)(Gemfile)
• Engines preferably share the same Ruby Engines preferably share the same Ruby version as Rails Appversion as Rails App
Typical Development Process1.1.Make changes in engine, rake, and commit Make changes in engine, rake, and commit
obtaining a new GIT refobtaining a new GIT ref
2.2.Update Gemfile in app with new git ref, run Update Gemfile in app with new git ref, run “bundle install” (getting ride of symlink)“bundle install” (getting ride of symlink)
3.3.Rake and commit changes in app.Rake and commit changes in app.
4.4.If more changes in engine are needed go back If more changes in engine are needed go back to step 1to step 1
Improved Development Process1.1. Work in app and engine until done WITHOUT running Work in app and engine until done WITHOUT running
“bundle install”“bundle install”
2.2. Rake and commit changes in engine obtaining a new git Rake and commit changes in engine obtaining a new git refref
3.3. Update Gemfile in app with git ref, run “bundle install”Update Gemfile in app with git ref, run “bundle install”
4.4. Rake and commit changes in app Rake and commit changes in app
Engines Reuse Engines
Rails engines can reuse other Rails enginesRails engines can reuse other Rails engines
Engines Reuse Engines
When multiple levels of depth are involved, When multiple levels of depth are involved, commit repos and update Gemfile from the commit repos and update Gemfile from the bottom up bottom up
Example:Example: Engine 2 => Engine 1 => AppEngine 2 => Engine 1 => App
1.1. Commit in Engine 2Commit in Engine 2
2.2. Update Gemfile in Engine 1 and commitUpdate Gemfile in Engine 1 and commit
3.3. Update Gemfile in App and commitUpdate Gemfile in App and commit
Rails Engine Patterns
Goals: Goals:
Keep engine code agnostic of app customizationsKeep engine code agnostic of app customizations
Prevent bi-directional coupling to simplify Prevent bi-directional coupling to simplify reasoning about codereasoning about code
Avoid app dependent conditionals to improve Avoid app dependent conditionals to improve code maintainabilitycode maintainability
Pattern - Common Domain
Problem: Multiple Rails Apps need to share a basic Problem: Multiple Rails Apps need to share a basic domain model including default CRUD and domain model including default CRUD and presentation behaviorpresentation behavior
Example: need to reuse address model and form Example: need to reuse address model and form entry behaviorentry behavior
Pattern - Common Domain
Solution:Solution:
In engine, include basic domain models (base In engine, include basic domain models (base behavior, common associations) with their views, behavior, common associations) with their views, CRUD controllers, and routes. CRUD controllers, and routes.
In each app, define domain model specialized In each app, define domain model specialized behavior, extra associations, and custom viewsbehavior, extra associations, and custom views
Make sure routes are declared with Make sure routes are declared with MyEngine::Engine.routes.draw (not Rails App MyEngine::Engine.routes.draw (not Rails App name)name)
Pattern - Common Domain
Example: address.rb, addresses_controller.rb, Example: address.rb, addresses_controller.rb, address route, and _address.html.erbaddress route, and _address.html.erb
Pattern - Expose Helper
Problem: need to customize presentation Problem: need to customize presentation logiclogic for a for a view in one app only, but keep the same logic in view in one app only, but keep the same logic in othersothers
Example: Example: One App needs to hide address1 and county for non-One App needs to hide address1 and county for non-government users.government users.
Other Apps wants to keep the fields displayed.Other Apps wants to keep the fields displayed.
You might start by overriding view, but then realize it You might start by overriding view, but then realize it is duplicating many elements just to hide a couple is duplicating many elements just to hide a couple fields.fields.
Pattern - Expose Helper
Solution: Solution:
In Engine, extract helper logic that needs In Engine, extract helper logic that needs customization into its own helper. customization into its own helper.
In App, redefine that new helper with In App, redefine that new helper with customizations.customizations.
Pattern - Expose Helper(view in Engine)
Pattern - Expose Helper (trying to customize view in App)
Pattern - Expose Helper
Remove custom view from AppRemove custom view from App
Use requires_extended_address? helper in Engine Use requires_extended_address? helper in Engine wherever the App used government_user? wherever the App used government_user?
In Engine, define requires_extended_address? to In Engine, define requires_extended_address? to return truereturn true
In App, define requires_extended_address? to return In App, define requires_extended_address? to return government_user?government_user?
Pattern - Expose Helper(view + helper in Engine)
Pattern - Expose Partial
Problem: need to have different customizations Problem: need to have different customizations in one part of the view in multiple appsin one part of the view in multiple apps
Example: Address form Example: Address form
One App needs an extra neighborhood field One App needs an extra neighborhood field
Another App needs an extra region fieldAnother App needs an extra region field
Pattern - Expose Partial
Solution: Solution:
In Engine, extract view part that needs In Engine, extract view part that needs customization as a partial. customization as a partial.
In App, redefine that partial with customizations.In App, redefine that partial with customizations.
Pattern - Expose Partial
Example:Example:
Define _address_extra_fields partial with empty Define _address_extra_fields partial with empty content in Enginecontent in Engine
Redefine _address_extra_fields in Apps needing Redefine _address_extra_fields in Apps needing extra fieldsextra fields
Pattern - Extension Point
Problem: multiple Apps need to contribute data Problem: multiple Apps need to contribute data to a View in different placesto a View in different places
Example: multiple Apps need to add custom Example: multiple Apps need to add custom Rows in different spots of a List that comes Rows in different spots of a List that comes from an Enginefrom an Engine
Pattern - Extension Point
Engine defines only 3 elements in a list (About Engine defines only 3 elements in a list (About Me, News and Noteworthy)Me, News and Noteworthy)
1
2
3
Pattern - Extension Point
Solution: Solution:
In Engine, add Helper logic that looks up partials In Engine, add Helper logic that looks up partials in a specific ext directory, and based on file name in a specific ext directory, and based on file name (e.g. row_7.html.erb) insert into the right location (e.g. row_7.html.erb) insert into the right location in the View. in the View.
In App, define these partials with the right file In App, define these partials with the right file names and locations.names and locations.
Pattern - Extension Point
App 1 adds “nav_bar_list_ext/_row_1.html.erb”App 1 adds “nav_bar_list_ext/_row_1.html.erb”
1
2
3
4
Pattern - Extension Point
App 2 adds “nav_bar_list_ext/_row_4.html.erb”App 2 adds “nav_bar_list_ext/_row_4.html.erb”
1
2
3
4
Pattern - Configurable Features
Problem: different apps need different features Problem: different apps need different features from an engine in different combinationsfrom an engine in different combinations
Pattern - Configurable Features
Solution: Solution:
In Engine, add logic that looks up configuration In Engine, add logic that looks up configuration optionsoptions
In App, configure Engine by overriding In App, configure Engine by overriding configuration optionsconfiguration options
Pattern - Configurable Features
Example: Example:
Engine defines engine_config.yml Engine defines engine_config.yml
enable_address_extensions: trueenable_address_extensions: true
enable_address_headers: trueenable_address_headers: true
App overrides some options in engine_config.ymlApp overrides some options in engine_config.yml
Engine uses config options to customize behavior Engine uses config options to customize behavior using conditionalsusing conditionals
Rails Engine Costs
Overhead in establishing a new Rails Engine Overhead in establishing a new Rails Engine gem projectgem project
Continuous switching between projects and Continuous switching between projects and engines to get work doneengines to get work done
Upgrade of ref numbers in GemfileUpgrade of ref numbers in Gemfile
Rails Engine Benefits
Code reuse across all application layersCode reuse across all application layers
Better maintainability due to:Better maintainability due to:
Independent project codebases Independent project codebases
Cleanly defined boundaries between projects and Cleanly defined boundaries between projects and reusable components (engines)reusable components (engines)
Project tests get smaller and run fasterProject tests get smaller and run faster
Engines vs Services
Engines are better when:Engines are better when:
Reusing small MVC features, especially domain Reusing small MVC features, especially domain independent (e.g. Search Map)independent (e.g. Search Map)
Preferring to avoiding network and infrastructure Preferring to avoiding network and infrastructure overhead over establishing a serviceoverhead over establishing a service
Wanting to quickly extract and reuse a featureWanting to quickly extract and reuse a feature
Engines vs Services
Web Services are better when:Web Services are better when:
Reusing larger MVC features that depend on Reusing larger MVC features that depend on domain datadomain data
Need to consume feature in another Need to consume feature in another programming language or outside the programming language or outside the organization boundariesorganization boundaries
Need to scale up feature performance Need to scale up feature performance independently of the application (e.g. separate independently of the application (e.g. separate DB)DB)
Engines vs Services
To keep an easier to maintain Agile code base, To keep an easier to maintain Agile code base, start simple and then move incrementally start simple and then move incrementally towards a more complex architecture:towards a more complex architecture:
Extract an MVC feature as an engine firstExtract an MVC feature as an engine first
Convert engine into a service when the need Convert engine into a service when the need arisesarises
Review
Basics of Rails Engines Basics of Rails Engines
Rails Engine Patterns Rails Engine Patterns
Typical Development ProcessTypical Development Process
Summary of Benefits and Trade-OffsSummary of Benefits and Trade-Offs
More Info
httphttp://edgeapi.rubyonrails.org/classes/Rails/://edgeapi.rubyonrails.org/classes/Rails/Engine.htmlEngine.html
http://andymaleh.blogspot.com/2011/09/more-http://andymaleh.blogspot.com/2011/09/more-productive-rails-productive-rails-engine.htmlengine.html
http://stackoverflow.com/questions/2964050/raihttp://stackoverflow.com/questions/2964050/rails-engines-extending-functionality/2990539#ls-engines-extending-functionality/2990539#29905392990539
Contact
Code Painter Blog: Code Painter Blog: http://andymaleh.blogspot.comhttp://andymaleh.blogspot.com
Twitter: Twitter: @AndyMaleh@AndyMaleh
LinkedIn: Look up “Andy Maleh”LinkedIn: Look up “Andy Maleh”