active_module
Modules and Classes as first-class active record values!
ActiveModel/ActiveRecord implementation of the Module attribute type.
- Allows storing a reference to a
Module
orClass
in a:string
database field - Automatically casts strings and symbols into modules when creating and querying objects
- Symbols or strings refer to the modules using unqualified names
- It is safe and efficient
This is a very generic mechanism that enables many possible utilizations, for instance:
- Composition-based polymorphism (Strategy design pattern)
- Rapid prototyping static domain objects
- Static configuration management
- Rich Java/C#-like enums
You can find examples of these in Usage -> Examples.
TL;DR
Declare module attributes like this:
class MyARObject < ActiveRecord::Base
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyClass, Nested::Module]
end
Assign them like this:
object.module_field = Nested::Module
object.module_field = :Module
object.module_field = "Module"
object.module_field #=> Nested::Module:Module
Query them like this:
MyARObject.where(module_field: Nested::Module)
MyARObject.where(module_field: :Module)
MyARObject.where(module_field: "Module")
object.module_field #=> Nested::Module:Module
And compare them like this:
object.module_field == Nested::Module
module MyNameSpace
using ActiveModule::Comparison
object.module_field =~ :Module
object.module_field =~ "Module"
end
Installation
Add to your gemfile - and if you are using rails - that's all you need:
gem 'active_module', "~> 0.2"
If you are not using rails, just issue this command after loading active record
ActiveModule.register!
or this, if you prefer to have a better idea of what you are doing:
ActiveModel::Type.register(:active_module, ActiveModule::Base)
ActiveRecord::Type.register(:active_module, ActiveModule::Base)
Usage
Add a string field to the table you want to hold a module attribute in your migrations
create_table :my_ar_objects do |t|
t.string :module_field, index: true
end
Now given this random module hierarchy:
class MyARObject < ActiveRecord::Base
module MyModule1; end
module MyModule2; end
class MyClass;
module MyModule1; end
end
end
You can make the field refer to one of these modules/classes like this:
class MyARObject < ActiveRecord::Base
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
end
Optionally, you can specify how to map your modules into the database (the default is the module's fully qualified name):
attribute :module_field,
:active_module,
possible_modules: [MyModule1, MyModule2, MyClass, MyClass::MyModule1]
mapping: {MyModule1 => "this is the db representation of module1"}
And this is it! Easy!
Assigning and querying module attributes
Now you can use this attribute in many handy ways!
For instance, you may refer to it using module literals:
MyARObject.create!(module_field: MyARObject::MyModule1)
MyARObject.where(module_field: MyARObject::MyModule1)
my_ar_object.module_field = MyARObject::MyModule1
my_ar_object.module_field #=> MyARObject::MyModule1:Module
But as typing fully qualified module names is not very ergonomic, you may also use symbols instead:
MyARObject.create!(module_field: :MyClass)
MyARObject.where(module_field: :MyClass)
my_ar_object.module_field = :MyClass
my_ar_object.module_field #=> MyARObject::MyClass:Class
However, if there is the need for disambiguation, you can always use strings instead:
MyARObject.create!(module_field: "MyClass::MyModule1")
MyARObject.where(module_field: "MyClass::MyModule1")
my_ar_object.module_field = "MyClass::MyModule1"
my_ar_object.module_field #=> MyARObject::MyClass::MyModule::Module
Comparing modules with strings and symbols
In order to compare modules with Strings or Symbols you'll have to use the ActiveModule::Comparison
refinement. This refinement adds the method Module#=~
to the Module
class, but this change is
only available within the namespace that includes the refinement.
module YourClassOrModuleThatWantsToCompare
using ActiveModule::Comparison
def method_that_compares
my_ar_object.module_field =~ :MyModule1
end
end
or like this, if you don't want to use the refinement:
ActiveModule::Comparison.compare(my_ar_object.module_field, :MyModule1)
but in this last case it would probably make more sense to simply use a module literal:
my_ar_object.module_field == MyClass::MyModule1
Examples
Composition-based polymorphism (Strategy design pattern)
The Strategy design pattern allows composition based polymorphism. This enables runtime polymorphism (by changing the strategy in runtime), and multiple-polymorphism (by composing an object of multiple strategies).
If you want to use classes this will do:
class MyARObject < ActiveRecord::Base
attribute :strategy_class, :active_module, possible_modules: StrategySuperclass.subclasses
def strategy
@strategy ||= strategy_class.new(some_args_from_the_instance)
end
def run_strategy!(args)
strategy.call(args)
end
end
But if you are not in the mood to define a class hierarchy for it (or if you are performance-savy), you may use modules instead:
class MyARObject < ActiveRecord::Base
module Strategy1
def self.call
"strategy1 called"
end
end
module Strategy2
def self.call
"strategy2 called"
end
end
attribute :strategy,
:active_module,
possible_modules: [Strategy1, Strategy2]
def run_strategy!(some_args)
strategy.call(some_args, other_args)
end
end
MyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
You can later easily promote these modules to classes if you need instance variables:
class MyARObject < ActiveRecord::Base
class Strategy1
def self.call
self.new.call
end
def call
"strategy1 called"
end
end
module Strategy2
def self.call
"strategy2 called"
end
end
attribute :strategy,
:active_module,
possible_modules: [Strategy1, Strategy2]
def run_strategy!(some_args)
strategy.call(some_args, other_args)
end
end
MyARObject.create!(module_field: :Strategy1).run_strategy! #=> "strategy1 called"
MyARObject.create!(module_field: :Strategy2).run_strategy! #=> "strategy2 called"
Rapid prototyping static domain objects
# Provider domain Object
module Provider
# As if the domain model class
def self.all
[Ebay, Amazon]
end
# As if the domain model instances
module Ebay
def self.do_something!
"do something with the ebay provider config"
end
end
module Amazon
def self.do_something!
"do something with the amazon provider config"
end
end
end
class MyARObject < ActiveRecord::Base
attribute :provider,
:active_module,
possible_modules: Provider.all
end
MyARObject.create!(provider: :Ebay).provier.do_something!
#=> "do something with the ebay provider config"
MyARObject.create!(provider: Provider::Amazon).provider.do_something!
#=> "do something with the amazon provider config"
What is interesting about this is that we can later easily promote our provider objects into full fledged ActiveRecord objects without big changes to our code:
class Provider < ActiveRecord::Base
def do_something!
#...
end
end
class MyARObject < ActiveRecord::Base
belongs_to :provider
end
Just in case you'd like to have shared code amongst the instances in the above example, this is how you could do so:
# Provider domain Object
module Provider
# As if the domain model class
def self.all
[Ebay, Amazon]
end
module Base
def do_something!
"do something with #{something_from_an_instance}"
end
end
# As if the domain model instances
module Ebay
include Base
extend self
def something_from_an_instance
"the ebay provider config"
end
end
module Amazon
include Base
extend self
def something_from_an_instance
"the amazon provider config"
end
end
end
Static configuration management
This example is not much different than previous one. It however stresses that the module we refer to might be used as a source of configuration parameters that change the behaviour of the class it belongs to:
# Provider domain Object
module ProviderConfig
module Ebay
module_function
def url= 'www.ebay.com'
def number_of_attempts= 5
end
module Amazon
module_function
def url= 'www.amazon.com'
def number_of_attempts= 10
end
def self.all
[Ebay, Amazon]
end
end
class MyARObject < ActiveRecord::Base
attribute :provider_config,
:active_module,
possible_modules: ProviderConfig.all
def load_page!
n_attempts = 0
result = nil
while n_attempts < provider.number_of_attempts
result = get_page(provider.url)
if(result)
return result
else
n_attempts.inc
end
end
result
end
end
MyARObject.create!(provider_config: :Ebay).load_page!
Rich Java/C#-like enums
This example is only to show the possibility.
This would probably benefit from using a meta programming abstraction
and there are already gems with this kind of functionality such as enumerizable
In a real world project, I guess it would rather make sense to extend ActiveModule::Base
or even ActiveModel::Type::Value
. But here it goes for the sake of example.
Java/C# enums allow defining methods on the enum, which are shared across all enum values:
module PipelineStage
module_function
def all
[InitialContact, InNegotiations, LostDeal, PaidOut]
end
def cast(stage)
self.all.map(&:external_provider_code).find{|code| code == stage} ||
self.all.map(&:database_representation).find{|code| code == stage} ||
self.all.map(&:frontend_representation).find{|code| code == stage}
end
module Base
def external_provider_code
@external_provider_code ||= self.name.underscore
end
def database_representation
self.name
end
def frontend_representation
@frontend_representation ||= self.name.demodulize.upcase
end
end
module InitialContact
extend Base
end
module InNegotiations
extend Base
end
module LostDeal
extend Base
end
module PaidOut
extend Base
end
end
class MyARObject < ActiveRecord::Base
attribute :pipeline_stage,
:active_module,
possible_modules: PipelineStage.all
end
object = MyARObject.new(pipeline_stage: :InitialStage)
object.pipeline_stage&.frontend_representation #=> "INITIAL_STAGE"
object.pipeline_stage = :InNegotiations
object.pipeline_stage&.database_representation #=> "PipelineStage::InNegotiations"
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and the created tag, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/pedrorolo/active_module.