Chaining autoloaded classes in Ruby - ruby

I have a class, User in user.rb that will be autoloaded as needed by the following statement:
autoload :User, 'models/user.rb'
This model is shared between a few different codebases (as a Git submodule, if that makes a difference). In one such codebase, I have a need to reopen the User class and add some methods. Where this gets complicated, for me at least, is that I need the resultant, extended class to be autoloaded in place of the original class.
Is there a pattern for chaining autoloaded classes in Ruby? Something like:
autoload :User, ['models/user.rb', 'extended_models/user.rb']
Or should I be using inheritance instead of monkey-patching? I'm open to suggestions. Thanks in advance.

Here's what I ended up doing: my main file autoloads the extended class, then the extended class autoloads the base class. Reopening the class triggers the second autoload. Although a little clumsy, this lets me keep my base classes pristine while preserving autoloading behavior. (Autoloading is a requirement for me because the database isn't available for Sequel ORM to discover the table schemas when the app first fires up.)
It looks like this:
main.rb:
autoload :User, 'extended_models/user.rb'
extended_models/user.rb:
autoload :User, '../models/user.rb'
class User
def self.from_omniauth(authhash)
# ...
end
end
models/user.rb:
class User < Sequel::Model
# ...
end
I have some helper functions that help me autoload directories of models and single models with relative paths, but this is the general idea.

If its shared between several codebases, it would probably be the best to make it a Module instead of a class.

Related

Rails: monkey-patching ActiveRecord::Base vs creating a Module

I am reading through The Rails 4 way (by Obie Fernandez), a well-known book about Rails, and from what I've read so far, I can highly recommend it.
However, there is an example section 9.2.7.1: Multiple Callback Methods in One Class that confuses me:
Bear with me, to make the problem clear for everyone, I have replicated the steps the book describes in this question.
The section talks about Active Record callbacks (before_create, before_update and so on), and that it is possible to create a class that handles multiple callbacks for you. The listed code is as follows:
class Auditor
def initialize(audit_log)
#audit_log = audit_log
end
def after_create(model)
#audit_log.created(model.inspect)
end
def after_update(model)
#audit_log.updated(model.inspect)
end
def after_destroy(model)
#audit_log.destroyed(model.inspect)
end
end
The book says then that to add this audit logging to an Active Record class, you would do the following:
class Account < ActiveRecord::Base
after_create Auditor.new(DEFAULT_AUDIT_LOG)
after_update Auditor.new(DEFAULT_AUDIT_LOG)
after_destroy Auditor.new(DEFAULT_AUDIT_LOG)
...
end
The book then notes that this code is very ugly, having to add three Auditors on three lines, and that it not DRY. It then goes ahead and tells us that to solve this problem, we should monkey-patch an acts_as_audited method into the Active Record::Base object, as follows:
(the book suggests putting this file in /lib/core_ext/active_record_base.rb)
class ActiveRecord::Base
def self.acts_as_audited(audit_log=DEFAULT_AUDIT_LOG)
auditor = Auditor.new(audit_log)
after_create auditor
after_update auditor
after_destroy auditor
end
end
which enables you to write the Account Model class as follows:
class Account < ActiveRecord::Base
acts_as_audited
...
end
Before reading the book, I have already made something similar that adds functionality to multiple Active Record models. The technique I used was to create a Module. To stay with the example, what I have done was similar to:
(I would put this file inside /app/models/auditable.rb)
module Auditable
def self.included(base)
#audit_log = base.audit_log || DEFAULT_AUDIT_LOG #The base class can override it if wanted, by specifying a self.audit_log before including this module
base.after_create audit_after_create
base.after_update audit_after_update
base.after_destroy audit_after_destroy
end
def audit_after_create
#audit_log.created(self.inspect)
end
def audit_after_update
#audit_log.updated(self.inspect)
end
def audit_after_destroy
#audit_log.destroyed(self.inspect)
end
end
Note that this file both replaces the Auditor and the monkey-patched ActiveRecord::Base method. The Account class would then look like:
class Account < ActiveRecord::Base
include Auditable
...
end
Now you've read both the way the book does it, and the way I would have done it in the past. My question: Which version is more sustainable in the long-term? I realize that this is a slightly opinionated question, just like everything about Rails, but to keep it answerable, I basically want to know:
Why would you want to monkey-patch ActiveRecord::Base directly, over creating and including a Module?
I would go for the module for a few reasons.
Its obvious; that is to say, I can quickly find the code that defines this behavior. In acts_as_* I don't know if its from some gem, library code, or defined within this class. There could be implications about it being overridden or piggy-backed in the call-stack.
Its portable. It uses method calls that are commonly defined in libraries that define callbacks. You could conceivably distribute and use this library in non-active-record objects.
It avoids the addition of unnecessary code on the static level. I'm a fan of having less code to manage (less code to break). I like using Ruby's niceties without doing to much to force it to be "nicer" than it already it is.
In a monkey-patch setting you are tying the code to a class or module that could go away and there are scenarios where it would fail silently until your class can't call acts_as_*.
One downfall of the portability argument is the testing argument. In which case I would say you can write your code to protect against portability, or fail early with smart warnings about what will and won't work when used portably.

How do I require classes with a circular dependency? [duplicate]

This question already has answers here:
Circular Dependencies in Ruby
(3 answers)
Closed 9 years ago.
I've been spoiled by Rail's autoloading of missing constants. In Ruby, if I have two classes, one nested inside the other but in different files, how do I require them since both depend on each other (circular dependency).
# user.rb
class User < ActiveRecord::Base
serialize :preferences, User::Preferences
end
# user/preferences.rb
class User::Preferences
end
# user_spec.rb
require 'user'
require 'user/preferences'
Note: I have not required the Rails environment.
If I try and load User first, the code fails because it does not know about User::Preferences yet. If I load "user/preferences" first, it fails when it loads User because the existing User class does not subclass ActiveRecord.
I have a suspicion I need to remove the circular dependency or, if possible, make serialize lazy load the class by passing a string 'User::Preferences' which is turned in to a constant when needed.
One hack I have is to create an empty User class inheriting from ActiveRecord::Base in user/preferences.rb:
class User < ActiveRecord::Base; end
class User::Preferences
end
Rather than wire knowledge of User's implementation into User::Preferences you could put the stub declaration in a common base, like so:
# user_base.rb
class User < ActiveRecord::Base; end
# user.rb
require 'user_base'
require 'user/preferences'
class User
serialize :preferences, User::Preferences
...
end
# user/preferences.rb
require 'user_base'
class User::Preferences
end
Alternatively, you could move User::Preferences into an independent module namespace such as ModelHelper::User::Preferences. I think I prefer this solution. The fact that you have a circular dependency is a code smell and the only thing causing it is the reuse of User class as a namespace container for User::Preferences.

Including module in file vs. in class (Ruby Mixins): What is better?

In Ruby I want to put some helpers in an extra-module and file. I will use it at the model-level, so i put
include MyHelper
class Shop
...
end
I now realize this can also be done like so:
class Shop
include MyHelper
...
end
Can anyone explain me more about the differences? Is the second version always the better choice?
The difference is that in the first case you include the helper in the global namespace. I can't think of a case in which that is a good idea.
In the latter case you include the helper in the "namespace" of the class. The (instance) methods defined in the module are now available to instances of the Shop class.

Location of a class in rails app

I want to extend the functionality of Array, add a method that checks if a key exists in array and that the array is not empty, where to write the class and how to make sure it's loaded?
You can either put it into lib/ and make sure that it is autoloaded as outlined in the answer by shioyama; or you could just put it into an initializer. I like the initializer approach a bit better, since it is easier (you get autoloading for free).
I usually create a core_ext subdirectory of the initializers directory and put my core class extensions in there. I always try to put the name of the class that is being extended and a description of what I add into the filename, so in you case I would create a file RAILS_ROOT/config/initializers/core_ext/array_my_function containing:
module MyFunctionForArray
def my_function(arg1, arg2)
# ...
end
end
Array.send :include, MyFunctionForArray
I always try to not reopen the class and extend it directly but to put my extensions into a module and then including this module into the class to extend.
Standard way to do it is to put the code in lib/ and make sure it's autoloaded by rails by adding a line to config/application.rb:
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Then in your code, just make sure you require it wherever you use it. If you want to apply it everywhere, create an initializer in config/initializers with a line:
require 'my_array'
Where my_array.rb is the name of the file in lib where you have the file. That will make it available in your models, controllers, views, etc.
See also: Best way to load module/class from lib folder in Rails 3?
Also, beware of one pitfall of autoloading a directory structure in ruby (not just rails), explained in this answer: Best way to load module/class from lib folder in Rails 3?

Applying how ActiveRecord uses modules to other Ruby projects

I decided to dig into the ActiveRecord code for Rails to try and figure out how some of it works, and was surprised to see it comprised of many modules, which all seem to get included into ActiveRecord::Base.
I know Ruby Modules provide a means of grouping related and reusable methods that can be mixed into other classes to extend their functionality.
However, most of the ActiveRecord modules seem highly specific to ActiveRecord. There seems to be references to instance variables in some modules, suggesting the modules are aware of the internals of the overall ActiveRecord class and other modules.
This got me wondering about how ActiveRecord is designed and how this logic could or should be applied to other Ruby applications.
It is a common 'design pattern' to split large classes into modules that are not really reusable elsewhere simply to split up the class file? Is it seen as good or bad design when modules make use of instance variables that are perhaps defined by a different module or part of the class?
In cases where a class can have many methods and it would become cumbersome to have them all defined in one file, would it make as much sense to simply reopen the class in other files and define more methods in there?
In a command line application I am working on, I have a few classes that do various functions, but I have a top level class that provides an API for the overall application - what I found is that class is becoming bogged down with a lot of methods that really hand off work to other class, and is like the glues that holds the pieces of the application together. I guess I am wondering if it would make sense for me to split out some of the related methods into modules, or re-open the class in different code files? Or is there something else I am not thinking of?
I've created quite a few modules that I didn't intend to be highly reusable. It makes it easier to test a group of related methods in isolation, and classes are more readable if they're only a few hundred lines long rather than thousands. As always, there's a balance to be struck.
I've also created modules that expect the including class to define instance methods so that methods defined on the module can use them. I wouldn't say it's terribly elegant, but it's feasible if you're only sharing code between a couple of classes and you document it well. You could also raise an exception if the class doesn't define the methods you want:
module Aggregator
def aggregate
unless respond_to?(:collection)
raise Exception.new("Classes including #{self} must define #collection")
end
# ...
end
end
I'd be a lot more hesitant to depend on shared instance variables.
The biggest problem I see with re-opening classes is simply managing your source code. Would you end up with multiple copies of aggregator.rb in different directories? Is the load order of those files determinate, and does that affect overriding or calling methods in the class? At least with modules, the include order is explicit in the source.
Update: In a comment, Stephen asked about testing a module that's meant to be included in a class.
RSpec offers shared_examples as a convenient way to test shared behavior. You can define the module's behaviors in a shared context, and then declare that each of the including classes should also exhibit that behavior:
# spec/shared_examples/aggregator_examples.rb
shared_examples_for 'Aggregator' do
describe 'when aggregating records' do
it 'should accumulate values' do
# ...
end
end
end
# spec/models/model_spec.rb
describe Model
it_should_behave_like 'Aggregator'
end
Even if you aren't using RSpec, you can still create a simple stub class that includes your module and then write the tests against instances of that class:
# test/unit/aggregator_test.rb
class AggregatorTester
attr_accessor :collection
def initialize(collection)
self.collection = collection
end
include Aggregator
end
def test_aggregation
assert_equal 6, AggregatorTester.new([1, 2, 3]).aggregate
end

Resources