Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zeitwerk-based, efficient, decorators loader #60

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

elia
Copy link
Member

@elia elia commented Mar 9, 2021

This work is built on the awesome work by @fxn on zeitwerk for Rails and @aldesantis on prependers for Solidus

The code in this PR has been tried successfully on a couple of real-world projects with multi-year legacy. The basic principle is that we should only load the decorators for the base classes that are required for the current request, finally playing nice with Rails own autoloading/reloading mechanisms.

One thing that was very interesting is seeing classes autoloaded from initializers (which is an anti-pattern) and the ripple effect they create in terms of loading their dependencies, fixing that kind of stuff would make Solidus stores startup virtually as fast as a pristine Rails app.

As a side note this work will be also proposed for the Rails guides that currently suggest eagerly loading all the decorators, we think, for lack of a better alternative.

from the inline documentation

SolidusSupport::Decorators is an efficient decorators loader that will extend the behavior of existing
classes and modules without eagerly loading all of your decorators at each request.

It nicely falls back to reloading them all when zeitwerk is not available, but will only load the decorators for a
given base class when it can. This means that, on an average Solidus application, instead of loading 171 decorators
and 61 base classes with all their dependency it will just load the ones needed by the current request, or, if
starting a console, almost nothing.

It will also prevent some nasty edge cases in which the use of Rails.application.config.to_prepare(&…) would do
some things twice, messing up calls to super inside decorators (to_prepare is actually called twice under some
circumstances).

Migrating from Prependers:

require 'solidus_support/decorators'
SolidusSupport::Decorators.autoload_decorators(Rails.root.join("app/prependers/**/*.rb"), autoprepend: true) do |path|
  relative = path.relative_path_from(Rails.root.join("app/prependers")) # models/spree/order/add_feature.rb
  parts = relative.to_s.split(File::SEPARATOR)
  {
    # remove models/
    # => "AcmeCorp::Spree::Order::AddFeature"
    decorator: parts[1..-1].join("/").sub(/\.rb$/,'').camelize, # "AcmeCorp::Spree::Order::AddFeature"

    # remove models/acme_corp/ and /add_feature.rb
    # => "Spree::Order"
    base: parts[2..-2].join("/").camelize, # "Spree::Order"
  }
end

Migrating from classic Solidus decorators

require 'solidus_support/decorators'
SolidusDecorators.autoload_decorators("#{Rails.root}/app/**/*_decorator.rb") do |path|
  relative_path = path.relative_path_from(Rails.root.join("app/")) # models/acme_corp/order_decorator.rb
  parts = relative_path.to_s.split(File::SEPARATOR)
  {
    # remove models/acme_corp/ and _decorator.rb, add spree/
    # => "Spree::Order"
    base: (["spree"] + parts[2..-1]).join("/").chomp("_decorator.rb").camelize,

    # remove models/
    # => "AcmeCorp::Spree::Order::AddFeature"
    decorator: parts[1..-1].join("/").chomp(".rb").camelize,
  }
end

A more complex example with legacy mixed behaviors

require 'solidus_support/decorators'
SolidusSupport::Decorators.autoload_decorators("#{Rails.root}/app/**/*_decorator.rb", autoprepend: false) do |path|
  case path.to_s
  when /lockable_decorator/
    nil # not a real decorator
  when /carton_decorator/
    {
      base: "Spree::Carton",
      decorator: "AcmeCorp::CartonDecorator",
    }
  when /devise_controller/
    {
      base: "DeviseController",
      decorator: "AcmeCorp::DeviseControllerDecorator",
    }
  when /inventory_unit_finalizer/
    {
      base: "Spree::Stock::InventoryUnitsFinalizer",
      decorator: "AcmeCorp::Stock::InventoryUnitFinalizerDecorator"
    }
  else
    relative_path = path.relative_path_from(Rails.root.join("app/")) # models/acme_corp/order_decorator.rb
    parts = relative_path.to_s.split(File::SEPARATOR)
    {
      base: (["spree"] + parts[2..-1]).join("/").chomp("_decorator.rb").camelize, # => "Spree::Order"
      decorator: parts[1..-1].join("/").chomp(".rb").camelize, # => "AcmeCorp::OrderDecorator"
    }
  end
end

@elia elia force-pushed the elia+spaghetticode/decorators-loader branch 2 times, most recently from 6b4c1ee to 6ba6c9b Compare March 9, 2021 09:20
@kennyadsl
Copy link
Member

I like this a lot!

It's not clear if the current change is backward compatible though. Do we need to change all stores/extensions to make it work once we merge/release this?

@elia
Copy link
Member Author

elia commented Mar 9, 2021

It supports both zeitwerk and classic and can be adapted to cover existing stores and extensions.
That said, I think we should make it opt-in, like maybe a strong suggestion, but still a suggestion 😊

@elia elia marked this pull request as ready for review March 11, 2021 11:37
@elia elia force-pushed the elia+spaghetticode/decorators-loader branch from 6ba6c9b to eb07e8f Compare April 16, 2021 15:48
@elia elia force-pushed the elia+spaghetticode/decorators-loader branch from eb07e8f to 7b1bee3 Compare April 30, 2021 10:09
@stale
Copy link

stale bot commented Nov 11, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Nov 11, 2022
@gsmendoza gsmendoza removed the wontfix label Nov 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants