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

Designing and Implementing Modern Rails Plugins

Designing and Implementing Modern Rails Plugins

Slides for RailsClub 2016 talk "Designing and Implementing Modern Rails Plugins" http://railsclub.ru/

Akira Matsuda

October 22, 2016
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. Agenda Dealing with Frameworks Through Adapters Automagic Standing on Shoulders

    of Rails Using Modern Ruby Features Make Rails Engines Great Again From Omakase to Okonomi
  2. Kaminari ⚡ /amatsuda/kaminari Adds some pagination methods to models Provides

    some view helpers Supports multiple ORMs and web frameworks
  3. Layers M and V Implemented as a Rails Engine Actually

    one of the first generation Rails Engine plugins Devise 1.1.0: July 25, 2010 Kaminari 0.1.0: February 5, 2011
  4. Supported Frameworks 0.1: Active Record & Action View (default Rails

    stack) 0.9.13: Mongoid 0.12.2: MongoMapper 0.13.0: DataMapper 0.13.0: Sinatra 0.14.0: Grape
  5. How Kaminari Loads the Frameworks No dependency in the gemspec

    Just `require` from the ruby code if defined?(the framework) then load the adapter
  6. lib/kaminari/ hooks.rb (0.16.x) module Kaminari class Hooks def self.init ...

    begin; require 'mongoid'; rescue LoadError; end if defined? ::Mongoid require 'kaminari/models/mongoid_extension' ::Mongoid::Document.send :include, Kaminari::MongoidExtension::Document ennnnd
  7. Such a Nasty Code! Just ugly ::Mongoid can be defined

    even if not using Mongoid gem Users have to install some unrelated code that they don't use Takes so much time to run CI
  8. The CI Problem kaminari 0-17-stable branch runs CI against Ruby

    1.9.3, 2.0.0, 2.1.10, 2.2.5, 2.3.1, jruby-1.7.25, jruby-9.1.0.0, rbx-2 active_record_30, active_record_31, active_record_32, active_record_40, active_record_41, active_record_42, active_record_edge, data_mapper_12, mongo_mapper, mongoid_24, mongoid_30, mongoid_31, mongoid_40, mongoid_50, sinatra_13, sinatra_14 Wasting Travis's resource
  9. Separating Core and Adapters So I decided to split up

    the huge monolithic gem into the core gem and adapter gems
  10. Bundling Kaminari with Active Record + Action View (the Default

    Rails Stack) gem 'kaminari-activerecord' gem 'kaminari-actionview'
  11. Active Record + Action View = the Default Rails Stack

    99% of Kaminari users should be using Kaminari with these gems This should be able to be done easier
  12. Using Kaminari with Active Record + Action View (Rails) gem

    'kaminari-activerecord' gem 'kaminari-actionview'
  13. Using Kaminari with Active Record + Action View (Rails) #

    gem 'kaminari-activerecord' # gem 'kaminari-actionview' gem 'kaminari'
  14. kaminari Gem A meta package that depends on kaminari-activerecord and

    kaminari-actionview Just like rails gem and rspec gem
  15. Compatibilty Those who uses kaminari with the default Rails stack

    doesn't have to change what's written in Gemfile
  16. Q. What If I Need a Pagination Library for Hanami?

    Just create kaminari-hanami gem! And let me know! I'm happy to put it under /kaminari organization
 (if you'd like to) This should be much easier now, I guess
  17. 1.0 Plan Pretty close! Some PRs to merge, some issues

    to solve We'll move the GH repo under kaminari organization Then release 1.0
  18. Maintaining a Gem Is Hard I create gems to solve

    my own problems Dealing with PRs/issues is to deal with other people's problems (Without being paid) Which is not always fun Maybe I'm not a very good gem maintainer
  19. A Software Needs "Mainainters" Just like nobu for ruby, and

    rafael for rails Luckily, Kaminari has @yuki24
  20. Kaminari kaminari 1.0 gem is coming soon! We're maintaining kaminari

    and kaminari-* gems as a team Join us! PRs are welcome!
  21. Just bundle, Then It Should Work Don't let the users

    configure for the plugin Don't let the users include some special Modules from the plugin Don't let the users call some weird method to let the plugin work These could be done via callbacks that Rails provides
  22. /amatsuda/ active_decorator Provides a decorator layer to Rails apps app/decorators/*_decorator.rb

    A decorator is essentially a view helper
 (alternative to app/helpers/*_helper.rb) A Decorator is a pure Ruby Module Mixed in to model instances via `Module#extend` Automagically only in Action View's context
  23. Draper Example # app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate end

    # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  24. Why Didn't I Send PRs to the Existing One? People

    ask me "why did you create your own paginater/decorator?" I DO sending PRs! Of course I usually consider doing this first
  25. When to PR, When to Fork, and When to Build

    Your Own? You just found a bug, and you could fix it => send a PR You basically liked it, but you wanted to add big changes => fork You didn't like its design/philosophy => build your own
  26. When to Just Raise an Issue? Basically, don't. Write a

    patch instead of filling the textarea with words If you couldn't fix the bug you've found, just filing a failing test case is totally fine
  27. I Said, A Decorator Is a View Helper Draper decorates

    in the Controller layer Which is IMO wrong Decorators should automatically work in view files Just like *_helpers.rb Who wants to write `include ApplicationHelper` in every view file? Explicitly calling `.decorate` method is like doing this
  28. Requirements Decorators should not be enabled in models/ controllers context

    Decorators should be automatically enabled in views context Without adding anything to the users' application code How can we achieve this?
  29. Convention over Configuration A decorator module for Article class should

    be named ArticleDecorator, and be placed in app/decorators/article_decorator.rb The Rails way that you're all familiar with Everyone likes this over writing something like "config/decorators.xml", right?
  30. Callbacks module ActiveDecorator class Railtie < ::Rails::Railtie initializer 'active_decorator' do

    ActiveSupport.on_load :action_controller do require 'active_decorator/monkey/ abstract_controller/rendering' ::ActionController::Base.send :prepend, ActiveDecorator::Monkey::AbstractController::Rendering ennnnd
  31. AS.on_load Callback Executes the block right after ActionController::Base (in this

    example) is loaded So the users don't have to write include/prepend in their applications Very common technique
  32. What We're Going to Do We want to decorate objects

    that are passed from controllers to views Also, we want to decorate collections Decorate each model instance in AR::Relation objects that are passed from controllers to views But we don't want ActiveDecorator to kick AR queries because of doing this
  33. Decorating Objects in Between Conrollers and Views All controller ivars

    should be accessed via `view_assigns` as a Hash So let's just override this method to decorate each value
  34. Monkey-patching view_assigns module ActiveDecorator module Monkey module AbstractController module Rendering

    def view_assigns hash = super hash.each_value do |v| ActiveDecorator::Decorator.instance.decorate v end hash ennnnnd
  35. Decorating Each Model in AR::Relation Instances Between Conrollers and Views

    - @articles.each do |article| %p= article.title %p= article.published_date
  36. Like This? module ActiveDecorator::Monkey::AbstractController::Rendering def view_assigns hash = super hash.each_value

    do |v| if v.is_a? ActiveRecord::Relation v.each do |record| ActiveDecorator::Decorator.instance.decorate record end else ActiveDecorator::Decorator.instance.decorate v end end hash ennd
  37. This Breaks POLS "Principal of Less Side-effects" Rails plugins should

    avoid causing unwanted side effects If a decorator fires AR queries, that's a serious side effect
  38. The Solution Double monkey-patching When decorating an AR::Relation, extend its

    `records` method to dynamically decorate each record
  39. Like This! module ActiveDecorator class Decorator def decorate(obj) ... if

    defined?(ActiveRecord) && obj.is_a?(ActiveRecord::Relation) && !obj.is_a? (ActiveDecorator::RelationDecorator) if obj.respond_to?(:records) obj.extend ActiveDecorator::RelationDecorator end else d = decorator_for obj.class return obj unless d obj.extend d unless obj.is_a? d end end module RelationDecorator def records super.tap do |arr| ActiveDecorator::Decorator.instance.decorate arr end end ennd
  40. These Techniques Makes This Possible # app/controllers/articles_controller.rb def show @article

    = Article.find(params[:id]) end # app/decorators/article_decorator.rb class ArticleDecorator def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date published_at.strftime("%A, %B %e") end def emphatic content_tag(:strong, "Awesome") end end
  41. Instead of This # app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate

    end # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  42. Without Poluting the App with These Unneeded Extra Codes #

    app/controllers/articles_controller.rb def show @article = Article.find(params[:id]).decorate end # app/decorators/article_decorator.rb class ArticleDecorator < Draper::Decorator delegate_all def publication_status published? ? "Published at #{published_date}" : "Unpublished" end def published_date object.published_at.strftime("%A, %B %e") end def emphatic h.content_tag(:strong, "Awesome") end end
  43. AR::Enum in Rails 4.1 Added to AR by DHH Persists

    values into DB in Integer Provides human-readable methods and scopes
  44. I Basically Liked This Feature We very often hit this

    use-case in real-world apps It works But something is missing
  45. Did You Know That Rails Once Tried to Merge AASM

    as ActiveModel::StateMachine?
  46. AASM, state_machine Very "Heavy" design Providing DSL, defining ruby methods,

    holding values in Objects, persisting to DBs, etc.
  47. % tree lib/aasm lib/aasm !"" aasm.rb !"" base.rb !"" configuration.rb

    !"" core # !"" event.rb # !"" state.rb # %"" transition.rb !"" dsl_helper.rb !"" errors.rb !"" instance_base.rb !"" localizer.rb !"" persistence # !"" active_record_persistence.rb # !"" base.rb # !"" core_data_query_persistence.rb # !"" dynamoid_persistence.rb # !"" mongo_mapper_persistence.rb # !"" mongoid_persistence.rb # !"" plain_persistence.rb # !"" redis_persistence.rb # %"" sequel_persistence.rb !"" persistence.rb !"" rspec # !"" allow_event.rb # !"" allow_transition_to.rb # !"" have_state.rb # %"" transition_from.rb !"" rspec.rb !"" state_machine.rb !"" state_machine_store.rb %"" version.rb
  48. % tree lib/ state_machine lib/state_machine !"" assertions.rb !"" branch.rb !""

    callback.rb !"" core.rb !"" core_ext # %"" class # %"" state_machine.rb !"" core_ext.rb !"" error.rb !"" eval_helpers.rb !"" event.rb !"" event_collection.rb !"" extensions.rb !"" graph.rb !"" helper_module.rb !"" initializers # !"" merb.rb # %"" rails.rb !"" initializers.rb !"" integrations # !"" active_model # # !"" locale.rb # # !"" observer.rb # # !"" observer_update.rb # # %"" versions.rb # !"" active_model.rb # !"" active_record # # !"" locale.rb # # %"" versions.rb # !"" active_record.rb # !"" base.rb # !"" data_mapper # # !"" observer.rb # # %"" versions.rb # !"" data_mapper.rb # !"" mongo_mapper # # !"" locale.rb # # %"" versions.rb # !"" mongo_mapper.rb # !"" mongoid # # !"" locale.rb # # %"" versions.rb # !"" mongoid.rb # !"" sequel # # %"" versions.rb # %"" sequel.rb !"" integrations.rb !"" machine.rb !"" machine_collection.rb !"" macro_methods.rb !"" matcher.rb !"" matcher_helpers.rb !"" node_collection.rb !"" path.rb !"" path_collection.rb !"" state.rb !"" state_collection.rb !"" state_context.rb !"" transition.rb !"" transition_collection.rb !"" version.rb !"" yard # !"" handlers # # !"" base.rb # # !"" event.rb # # !"" machine.rb # # !"" state.rb # # %"" transition.rb # !"" handlers.rb # !"" templates # # %"" default # # %"" class # # %"" html # # !"" setup.rb # # %"" state_machines.erb # %"" templates.rb %"" yard.rb
  49. One Day, I Had to Implement a State Machine on

    My Rails 4 App Where we already had an Enum And I came up with an idea "What if I build a state machine feature on top of AR::Enum?"
  50. What AR::Enum Already Provides A simple syntax to define key

    & value pairs on AR models Persists the values to the backend DB Defines some human-readable query methods and methods to update values
  51. Half of the State Machine Features Are Already Implemented in

    AR::Enum I thought I could build a state machine library that I need in less code
  52. /amatsuda/ stateful_enum Extends AR's `enum` method to take a block

    Uses what I like about AR::Enum, fixes what I don't like, and adds what are missing
  53. Method Names class Conversation < ActiveRecord::Base enum status: [ :active,

    :archived ] end # conversation.update! status: 0 conversation.active! conversation.active? # => true # conversation.update! status: 1 conversation.archived! conversation.archived? # => true
  54. No! Method Names Should Be Verbs! A method to change

    the status to :active should be `activate`
  55. DB Design I didn't like the DB design of existing

    state machine gems They tend to save the state names in VARCHAR into DB I prefer persisting INTEGER values just like AR::Enum does
  56. Size of State Machine Plugins % cat aasm/lib/**/*.rb | wc

    -l 2589 % cat state_machine/lib/**/*.rb | wc -l 10671 % cat stateful_enum/lib/**/*.rb | wc -l 226
  57. Amazingly Short Code Because I didn't need to build everything

    from scratch statuful_enum stands on shoulders of AR::Enum And we don't support multiple ORMs If we need to, we probably will follow the same strategy with kaminari
  58. Stand on Shoulders of Rails Use Rails' built-in code Rather

    than creating everything from scratch
  59. Write Less Code This is, of course, a Rails' principal

    It's easier to read It's easier to patch So it's easier to maintain
  60. Supported Versions of Ruby & Rails Basically 2-3 generations Because

    both projects do not have enough resources to maintain more than that
  61. Ruby Developing: 2.4 Security & bug fixes: 2.3, 2.2 Security

    fixes: 2.1 2.0 and 1.x: No support. No security updates. Please don't use!
  62. Rails Developing: 5.1 Security & bug fixes: 5.0, 4.2 Security:

    4.1 4.0 and 3.x: No support. No security update. Please don't use!
  63. Gems Do Not Have To Provide Such a Wide Support!

    At least, a new major release doesn't have to support unsupported versions of Ruby / Rails Rails aggressively bumps supporting Ruby versions To keep the codebase maintainable To move the community forward I really this attitude Let's do this for your Rails plugins as well!
  64. What Would We Get If We Stop Supporting Old Rubies?

    We can use new features of Ruby!
  65. New Features of Recent Versions of Ruby 2.0: Module#prepend, keyword

    arguments, Refinements 2.1: Module#include / prepend as public methods, Binding#local_variable_get 2.2: Kernel#itself, Method#super_method 2.3: &., Hash#dig
  66. alias_method_chain vs Module#prepend Rails no longer has AMC Modern plugins

    have to use `Module#prepend` If you find a gem that still doesn't use `Module#prepend`, that should be an outdated gem
  67. How action_args Uses Refinements Defines a Module that refines Rails'

    core Class Uses the Module in the plugin internal Users would never able to access these plugin internal methods
  68. Defining a Module That Refines Rails' Core Class # lib/action_args/params_handler.rb

    module ActionArgs module ParamsHandler refine AbstractController::Base do def extract_method_arguments_from_params(method_name) ... ennnnd
  69. Using the Module in the Plugin Internal using ActionArgs::ParamsHandler module

    ActionArgs module AbstractControllerMethods def send_action(method_name, *args) ... values = extract_method_arguments_from_params method_name super method_name, *values ennnd AbstractController::Base.send :prepend, ActionArgs::AbstractControllerMethods
  70. These Methods Are Never Exposed to the Users A very

    clean way to
 monkey-patch Ruby/Rails core classes in your plugin
  71. You Can Define
 Absolutely "Plugin private"
 Methods This Way This

    is a very useful technique Check out /asakusarb/ action_args for more details
  72. By Using Recent Ruby Features You may be able to

    make your plugin code cleaner Use new Ruby features! Instead of supporting old Rubies
  73. Rails Engines Rails Engine is_a Rails plugin Rails Application is_a

    Rails Engine A Rails Engine can contain whole M, V, and C components A Rails Engine can contain routes
  74. Rails Engines Patterns The app and plugins The main app

    and sub apps Shared components and apps
  75. Typical Rails Engine Plugins Mount another Rails app (e.g. admin

    interface) onto users' applications (Not very much technically interesting)
  76. /amatsuda/erd You can mount an ER diagram app You can

    manipulate the DB schema by editing the models and columns from the browser
  77. Rails Engines Seems Somewhat Useful But still too simple Maybe

    we need some more features to make it truly useful I don't know what exactly Rails Engines is missing. We need to investigate
  78. So, I Made a Product to Investigate a New Usage

    of Rails Engines /amatsuda/motorhead
  79. An Application Feature as a Rails Engine A Rails Engine

    can contain models, views, controllers, and routes Maybe we can implement an independent feature as an Engine
  80. Normal Rails App normal_rails_app %"" app !"" controllers # !""

    feature1_controller.rb # %"" feature2_controller.rb %"" views !"" feature1 %"" feature2
  81. Motorhead motorhead %"" app %"" engines !"" feature1 # %""

    app # !"" controllers # # %"" feature1_controller.rb # %"" views # %"" feature1 %"" feature2 %"" app !"" controllers # %"" feature2_controller.rb %"" views %"" feature2
  82. Advantage of This Directory Structure It's easier to add /

    remove features Without causing code conflicts within you team
  83. Rails Engines for Feature Prototyping You can overwrite existing controller

    actions You can call the original action from the Engine via super Automatically fallbacks to super on error So we can deploy unstable code to production
  84. Not Only in This Direction But I believe Rails Engines

    can be a better mechanism I'd like to expand the capability of Rails Engines
  85. From Omakase To Okonomi Rails is shipped with DHH's assorted

    strong default stack which is called "Omakase" Rails allows users to choose alternative libs, e.g. Sequel instead of AR, Haml instead of ERB I want to expand this a little bit more
  86. Rails Became Mature Enough Supports everything in one codebase Became

    too huge and slow I sometimes miss Rails 1.x simplicity We don't need all Rails features for each of our apps As we're deploying the whole framework, we're loading so many unused things in production Just like Kaminari's case that I showed in this presentation
  87. Rather Than Huge Gems That Provide New Features Alternative parts

    that are compatible with the default Rails parts Just like DOS/V PC parts You can choose Intel or AMD, ATI or nvidia, just combine them then that'll make a PC Mainly for speed, memory usage
  88. There Should Be Some Hidden Needs, For Example I miss

    the speed of AR1 query I want a simpler I18n that runs faster. I don't use String interpolations. Our language doesn't have pluralization form. So I18n can be simpler I want English only Rails. No I18n is needed for my app I want AR model instance not to create tons of Objects per each attribute. Just a Hash like AR3 was enough My app doesn't have to support Windows. I want Rails that deals JSON only. My Rails app has no HTML view I don't care about Thread-safeness. I don't use threaded servers
  89. You Could Solve Them With Plugins! Bringing AR1 speed back

    to AR queries A simpler and faster I18n alternative A routing engine Faster view template resolver Faster template renderer that cannot handle any other encodings than UTF8 Faster `url_for` Faster HashWithIndifferentAccess
  90. It's Totally Fine to Implement a New Attributes API for

    AR Actually I this improvement! But let's provide a way to define another Attributes API that works as fast as previous version
  91. Rails Core Doesn't Have to Create and Maintain Everything The

    community will work on such alternative parts, if needed
  92. Back to Rails 1, or Merb Let the users be

    able to choose Rails components Cut off what you don't use To enhance performance
  93. Maybe We Could Apply This Idea on Ruby Language As

    Well? I don't know. I never seriously thought about that, but maybe
  94. "Modern Rails Plugins" Kaminari 1.0 is coming soon! Follow the

    Rails way, Ruby way Keep yourself and your code up to date Keep your code maintainable, sustainable