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:
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.rbin 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_examplesas 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: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: