Different models in your Rails application will often share a set of cross-cutting concerns. In Basecamp, we have almost forty such concerns with names like Trashable, Searchable, Visible, Movable, Taggable.
These concerns encapsulate both data access and domain logic about a certain slice of responsibility. Here’s a simplified version of the taggable concern:
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings
end
def tag_names
tags.map(&:name)
end
end
This concern can then be mixed into all the models that are taggable and you’ll have a single place to update the logic and reason about it.
Here’s a similar concern where all we add is a single class method:
# current_account.posts.visible_to(current_user)
module Visible
extend ActiveSupport::Concern
module ClassMethods
def visible_to(person)
where \
"(#{table_name}.bucket_id IN (?) AND
#{table_name}.bucket_type = 'Project') OR
(#{table_name}.bucket_id IN (?) AND
#{table_name}.bucket_type = 'Calendar')",
person.projects.pluck('projects.id'),
calendar_scope.pluck('calendars.id')
end
end
end
Concerns are also a helpful way of extracting a slice of model that doesn’t seem part of its essence (what is and isn’t in the essence of a model is a fuzzy line and a longer discussion) without going full-bore Single Responsibility Principle and running the risk of ballooning your object inventory.
Here’s a Dropboxed concern that we mix into just the Person model, which allows us to later to route incoming emails to be from the right person:
module Dropboxed
extend ActiveSupport::Concern
included do
before_create :generate_dropbox_key
end
def rekey_dropbox
generate_dropbox_key
save!
end
private
def generate_dropbox_key
self.dropbox_key = SignalId::Token.unique(24) do |key|
self.class.find_by_dropbox_key(key)
end
end
end
Now this is certainly not the only way to slice up chubby models. For Visible concern, you could have Viewer.visible(current_account.posts, to: current_user)
and encapsulate the query in a stand-alone object. For Dropboxed, you could have a Dropbox stand-alone class.
But I find that concerns are often just the right amount of abstraction and that they often result in a friendlier API. I far prefer current_account.posts.visible_to(current_user)
to involving a third query object. And of course things like Taggable that needs to add associations to the target object are hard to do in any other way.
It’s true that this will lead to a proliferation of methods on some objects, but that has never bothered me. I care about how I interact with my code base through the source. That concerns happen to mix it all together into a big model under the hood is irrelevant to the understanding of the domain model.
We’ve been using this notion of extracting concerns from chubby models in all the applications at 37signals for years. It’s resulted in a domain model that’s simple and easy to understand without needless ceremony. Basecamp Classic’s domain model is 8+ years old now and still going strong with the use of concerns.
This approach to breaking up domain logic into concerns is similar in some ways to the DCI notion of Roles. It doesn’t have the run-time mixin acrobatics nor does it have the “thy models shall be completely devoid of logic themselves” prescription, but other than that, it’ll often result in similar logic extracted using similar names.
In Rails 4, we’re going to invite programmers to use concerns with the default app/models/concerns and app/controllers/concerns directories that are automatically part of the load path. Together with the ActiveSupport::Concern wrapper, it’s just enough support to make this light-weight factoring mechanism shine. But you can start using this approach with any Rails app today.
Enjoy!
Peter Boling
on 18 Dec 12This is a great paradigm. I like how an included set of concerns gives a concise overview of a model’s functionality.
DHH
on 18 Dec 12Peter, that’s a big benefit. This smells a little like aspects as well, but it’s way more explicit. When you have class Post; include Taggable, Searchable, Visible; end, it gives you a wonderfully accessible way of thinking about the system and the roles that model plays.
Piotr Solnica
on 18 Dec 12Concerns approach can quickly fall flat on its face because of the tight coupling that you introduce by mixing into a single class lots of (quite often) unrelated functionalities. By doing that you may even introduce dependencies between the concerns that you won’t even notice until things break for one reason or another.
This requires you to test if an object behaves as expected in all sorts of scenarios vs having multiple objects responsible for just one thing that you can test in isolation and don’t care about anything else.
Recommending this approach is just so wrong. It’s OK to use mixins when you want to improve code organization but people forget that mixins in Ruby are a form of INHERITANCE. And concerns? This is just an abuse of inheritance.
I already see people joining a project and extending AR models with even more crap as it’s the new recommended way. Then they leave, new people come and do the same. Nobody is going to spend 3 minutes to think how to design the system and split responsibilities in a sane way. They will just add more concerns and call it a day. After 2 years of this practice you’re left with a bunch of thin models from the source code pov and a ton of weird mixins that you have no idea how to test.
Joseph Le Brech
on 18 Dec 12One thing I do notice about this approach if that if you have one concern shared between more than one model you would then have to have a migration for each model to add the dropbox_key.
What would be useful would be to have a way to organise migrations based on their concerns.
Say you now have many models with a dropbox_key but later on you want to remove dropbox functionality, you could have a rake task to tear down all the migrations for that concern and they you could delete that concern without leaving any orphaned columns in the database.
DHH
on 18 Dec 12Piotr, I have not observed the problem of dependency-introducing breakage in the wild. Exactly because the concerns are typically “unrelated functionalities”, it just doesn’t turn out to be a problem.
Yes, mixins are a form of inheritance. I’m fine with the idea that Person.is_a? Taggable. That’s by design.
Regarding the evolution of a domain model, that’s always a hard problem when you have a revolving door of employees. We have used this approach for about 8 years in the Basecamp code base and been so happy with the result that every subsequent app we’ve made has followed the same pattern with great results. We have many apps in our inventory that are 5+ years old.
Testing mixins is easy enough, though. Have a TaggableTest and test the data/behavior provided by that concern. Just like you would any other class.
Joe Van Dyk
on 18 Dec 12Piotr, if you have people entering and leaving a project constantly without putting any thought into the design, is there any approach that would result in a good system?
DHH
on 18 Dec 12Joseph, I haven’t run into that problem in the wild (yet?). I’m not convinced it’s a problem. We so rarely drop columns that I’m not sure it’s worth optimizing that act. Should it happen, adding a migration that DropDroboxKeysFromDropboxables would probably suffice.
Jason
on 18 Dec 12@Piotr: You may be right, but I’d love to have David’s problems that can result from this – millions of users, revenue, applications and platforms you can decide on how to build vs. being told by others (ie, managers, bosses, etc).
Personally, I like this approach. It just feels better to me than the oldie but goodie acts_as_* approach. I can see where this is headed and there’s always way to figure out how to test things. And, sometimes people actually do think more than 3 minutes on how they’re going to design and test stuff like this; and they still like it.
David, thanks again for a great post. I’ve loved working in Rails over the years and every time I try some other framework, I always find myself asking the questions “why can’t they do it this way, like in Rails?”. The fact that there’s just so many elegant ways to do things in Ruby & Rails always keeps me coming back. To me, this is yet another one of those things.
Jon Daniel
on 18 Dec 12I’ll admit that I was quick to hop on the “Concerns are Evil” bandwagon, but after trying to implement some of my code using Single Responsibility Principle I began to see the value in being able to break out bits of functionality that don’t necessitate their own class (or can be shared across multiple classes).
Seeing my ./lib directory balloon with lots of *er classes make me think that there has to be a better way of doing this. Granted the code was nearly organized I still felt like ./lib was becoming a junk drawer.
Single Responsibility Principle definitely has it uses but a zealot-like adherence will just result in a bloated object count and many less-than-ideal (if not downright confusing) implementations.
Concerns are just another tool in the toolbox. Knowing when to use the right tool is part of our jobs and forcing ourselves to always use tool X will only lead to problems. I’m sure we’ve all seen a codebase like that.
Henning Koch
on 18 Dec 12I am the author of the Modularity/ gem, which is basically parametrizable Concerns. While I have been advocating David’s approach and used it in many large Rails projects, I’ve learned that it has a sweet spot like everything else.
I agree with David that e.g. tucking away tagging-related methods in a concern is an awesome way to make your classes easier to understand. And I personally don’t want a ProjectTagRegistry to manage a project’s tags.
But you must be aware that not all concerns are free. Especially if a concern introduces callbacks to a record, this will impact the use and testing of other concerns. Symptoms of this are when you have trouble creating records in your tests, or when you are afraid to save a record because “who knows what that might trigger!”.
So use concerns responsibly. When you see a great opportunity to extract a service object, take it and refactor. And when in doubt, try it out in a branch and compare code before/after.
Sergey Potapov
on 18 Dec 12I already use this approach for couple of models in my project. And it’s helpful, but I should not be overused. I would suggested to use shared behaviour to test stuff extended with concerns, but David doesn’t like RSpec.
Piotr Solnica
on 18 Dec 12@David yeah it can work. I know. I’m happy for you, really. It’s just that I would never ever recommend this as a general practice. Not every project has the luxury of having good, responsible and trusted developers like you have. I’m sure you know that. There are plenty of environments where this approach will simply lead to a total disaster. It’s also a very limited approach – it can only work to some extent. Just like you said – you never experienced problems b/c of dependencies between the mixins. Then you’re lucky! I can totally see cases where this can become a problem pretty quickly.
So yeah. It can work. And sometimes it can break. That’s it.
DHH
on 18 Dec 12Piotr, Rails itself is designed exclusively with the “good, responsible, trusted developers” in mind. If it happens to work for “bad, irresponsible, untrustworthy developers” as well, that’s merely an inconsequential side effect. There are lots of development environments that focus explicitly on preventing that later group from doing much damage. Ruby on Rails was never intended to be among those.
But yes, like everything, it can work and it can break. Our task is to identify practices that work more than they break. I believe this is one of those.
Rinaldi Fonseca
on 18 Dec 12Hi David, I see some problems with that approach: You are designing your domain model splitting code in modules and just moving code around. If I want to understand how my Post class works I have to open lots of files and look at lots of places. You may end up with “fat models”.
Mike Pack
on 18 Dec 12@piotr, @dhh I can attest to dealing with projects for which this approach grew vastly out of hand. We had dependencies between concerns, dependencies on concern inclusion order, and a huge tendency to throw new methods in any of the given buckets.
My primarily distaste for this approach is that it generally gets construed as a proper way to organize models. Instead of considering other locations for behavior, the go-to means of organizing models was to break them into concerns. These generally aren’t cross-cutting concerns, these are merely means by which a developer would organize code. In that regard, concerns help you very little with slimming down models unless all of your behavior is cross-cutting.
I cringe at the thought of needing to fix this problem in future projects.
DHH
on 18 Dec 12Rinaldi, you can run that line of argument against all forms of separation of concerns. That to understand the whole application, you have to read the whole application. Yes, that’s true.
To understand all aspects of how Post works and how it interacts with your application, you will need to read all code that involves it.
But when things are broken out into concerns, you can delay learning about how Post is a Taggable until you actually want to know how the Post performs the Taggable role. That’s a huge win.
mhenrixon
on 18 Dec 12I for one really like this way of working and I’ve concerned myself with both good and bad extractions. The bad ones usually incorporate some type of callback against something that isn’t specific to the concern it belongs to.
All in all though I am incredibly happy with that approach of grouping things together. Great post, thanks for letting us have your thoughts on the subject!
KendallB
on 18 Dec 12I believe DCI, however, has one huge advantage – code reuse across projects. Rather than filling our Rails apps with concerns, we’re learning to keep the Rails ‘app/models’ directory thin itself.
Logic you put in concerns, we’re putting in “Role” modules that end up in gems. Then, we can mix in the roles’ behavior in each relevant Rails app.
Is there a simpler way to do that?
Matt De Leon
on 18 Dec 12David, how often do you find yourself breaking up models into concerns for the purpose of “slicing” but not necessarily sharing code? I use this technique heavily, such as creating a Message::Translatable concern that is mixed-into Message but nothing else. I like opening message.rb and, instead of feeling lost in a stew of methods, seeing a list of “core functions” of the message model (Translatable, Deliverable, etc.)
Eric
on 18 Dec 12@KendallB —I reuse concerns all the time. It’s pretty easy to make an engine with that provides the module you want to include; plus, if you package it up that way you can include associated migrations, javascripts, stylesheets, etc.
Eric
on 18 Dec 12I’m sure there must have been SVN posts with code examples since the redesign, but this is the first time I’ve really noticed how great it looks.
Rinaldi Fonseca
on 18 Dec 12When you say: ”... you can delay learning about how Post is a Taggable until …” I totally agree. I see a evolution.
But I also agree with Giles Bowkett argument when he says:
“It’s the difference between re-organizing your desk by creating a system to finish all your tasks — that would be abstraction, and DCI — vs. “re-organizing your desk” by taking everything on the desk, throwing it in a box, and then hiding the box in another room. That would be indirection, and Rails 3 concerns.”
DHH
on 18 Dec 12Matt, we do that a fair bit. We also use an approach called #concerning that we’re still playing around with how best to share in the Rails world. It’s not entirely baked, but here’s the code: https://gist.github.com/4330391 (Jeremy wrote it).
Rinaldi, I think that’s a funny metaphor but I don’t think it describes any meaningful difference. Whether you use Role or Concern is more a matter of when that logic is mixed in (up front or at runtime). Your code is no simpler to understand by delaying inclusion until runtime, imo.
Matt De Leon
on 18 Dec 12David, interesting approach. Agreed with the extra overhead of having separate files, but the advantage is the focus when viewing one file and, moreover, writing tests for that single file. Still, I’ll have to try Concerning at some point.
Joakim Kolsjö
on 18 Dec 12DHH: What I think the big difference is between including the world all the time (concerns) and including only the slice needed (roles) is that for any given task I only need to know what the role does and what the model does and no more.
Greg Funtusov
on 18 Dec 12Great! I definitely agree with using concerns for extraction of widely used parts of models and would love to see more developers use more rails goodness.
Anonymous Coward
on 18 Dec 12as a “plus size” person, title is a bit offensive, but I appreciate the post and will forego my afternoon french fries #weightable
Luca Guidi
on 18 Dec 12This technique may help to improve code organisation, but nothing more. When you extract a concern, you are yelling out of loud that a specific model has more than one responsibility. Now, you can decide to follow or not to follow the SRP principle, but that’s what your code is stating. Also, don’t forget that all that methods are included at the runtime; so, virtually, you haven’t any solved the problem.
Sometimes I had troubles while testing mixins, because they may involve callbacks, migrations, associations; so I often find myself to test the model which includes that aspect, rather than the pure expressed behaviors.
Of course, everything depends on the context of your app, if all the models are taggable, you don’t want to include `Taggable` everywhere, but probably to have a proper class to serve the purpose of tagging is better.
Jim Gay
on 18 Dec 12Concerns are similar to roles in DCI because they contain behavior and that’s it. But concerns don’t make models any less chubby. Your headline is catchy, but you’re not actually putting your models on a diet.
A DCI context object represents how objects collaborate in a use case. A Concern is only a small part of a use case for your system, so it doesn’t reveal why the concern is included. A context, on the other hand, has all the related behavior for the use case in the same place.
The difference is that when we rely only on concerns, we have to put together the pieces in our heads. And those pieces could be anywhere. There’s no clue to us about where or when these concerns are necessary. It’s a more difficult task depending on how complex the use case is and it forces you to go looking around for details.
Phil Castillo
on 18 Dec 12+1 for Luca
What problem are you solving?
Are you talking about reducing the size of the actual ruby file or the runtime characteristics of the model – i.e. the model still has the same class and instance methods at runtime and therefore it is still chubby?
Matt Wynne
on 18 Dec 12I recommend reading this recent post from Byran Helkamp which explores this same problem in a great deal of depth.
Braden Schaeffer
on 18 Dec 12Please keep the title!
Bob Sacamano
on 18 Dec 12Hiding the implementation is good, and separating the concerns is also good. But these two principles are useless without the third leg to stand on—namely, the principle of least surprise.
There’s many ways to skin a cat, and consequently many ways to encapsulate various concerns. The question is, how much of a surprise will your implementation come for the next developer? Will it feel natural, or contrived?
Lee
on 18 Dec 12Sounds like traits.
P8
on 19 Dec 12visible_to works nice because it’s a simple method. Once you’ll need to add edge cases (not uncommon with authorization) things can get ugly.
I also prefer whitelisting instead of blacklisting (especially with authorization). So instead of having to remember to always use visible_to where required, I’d prefer to use something like viewer.current_account.posts.
Yama
on 19 Dec 12I can see that Piotr Solnica is objecting to these appraoch as he proffers an alternative approach to putting your model on a diet: http://solnic.eu/2011/08/01/making-activerecord-models-thin.html
To my mind, both are approaches have their pro’s and con’s, so evaluate and use the appraoch that suites your use case.
Charles Sistovaris
on 19 Dec 12I use concerns quit a lot : I have a Category module with scopes and only one method, a Content module for managing markdown, a Transatable module, an Attachable module… Usualy they have few methods and are used on many AR models: name collusion is unlikely and I can sprinkle them around.
However if the logic I need to abstract requires many methods (and generic stuff like process, call etc), I use an encapsulating class instead of mixing in a module. The delegate (aka forwardable) macro provided by activesupport (delegate :num, :prenum, :postnum, :to => :matcher) comes in as an extremely easy to grasp utility for exposing only the necessarystuff without relying much on design patterns.
Ian Lotinsky
on 19 Dec 12Having employed many popular strategies over the past decade, this is the best I’ve used, by far. (In fact, the entire Takeout & Delivery engineering team at LivingSocial has been quite pleased with it.)
1. At a quick glance, you get a feel for a class’ essence and capabilities. 2. You have a clean method for containing (and sometimes removing) features. 3. It’s much easier to debug one class than it is to navigate around a labyrinth of needlessly nested or abstracted classes—with the same data passed again and again and again. 4. If you absolutely must override behavior from one concern by another, you can do it in a controlled manner. 5. It’s an easy path to generalize functionality already contained in one class if it’s time for it to graduate.
One thing that would be good to know is how you all test concerns. Too much mocking can create false test positives. We’ve landed on mocking plus testing the use of the concern with one class that includes it. This doesn’t feel entirely right though. How do you test concerns?
DHH
on 19 Dec 12Ian, we test the concern through a model that has it included. Don’t do any mocking for it. Works well!
Joakim Kolsjö
on 19 Dec 12Ian: An alternative to mocking would be to create a new class in the test, include the concern and test using that class. Make it an ActiveRecord object if you have to and use “set_table_name” too if necessary. Then you can test the concern in relative isolation without using stubs.
Things like “mock” and “stub” are just ways to make test doubles on the fly, you can build your own test doubles too.
Rafael Schaer
on 19 Dec 12It may be an odd question, but why not put it into a gem and maintain the code there, especially in the case of taggable? What’s the advantage to “taggable” gem inclusion? Its also reusable for several apps as well. Isn’t the example mix a bit confusing?
Jacob
on 20 Dec 12David,
Do you have concerns which interact directly with model attributes? In other words, a concern that requires a certain column to exist on the model. If so, does this become challenging to manage if you need to, say, add a new column to all Models that include a given concern?
Piotr Solnica
on 20 Dec 12@David I should probably add that concerns approach can work nicely if you make sure that there are no 2 or more mixins dealing with the same “part” of the model. Also having a rule that a mixin should only be concerned about the data + some simple domain logic can prevent you from going overboard with this approach.
I use mixins myself to distribute shared behavior across my AR models but my rule is that I only use those for persistence related concerns. I wouldn’t put a huge chunk of some domain specific logic into a concern. I would move it to a standalone class which is decoupled from persistence and doesn’t care if it uses an AR object or something else.
yonbergman
on 20 Dec 12Awesome post @dhh, We’ve been using concerns with controllers for quite some time. To a point that i’ve created a small wrapper for ActiveSupport::concern that I call Controller Supports to easily enable to declare before_filters and helper_methods in such a way that when the mixin is included into other types of classes (Mailers or Test Stubs) it won’t fail. https://github.com/yonbergman/controller_support
Chris
on 20 Dec 12What the #pluck? I like it.
Mihael
on 20 Dec 12dear David,
well, it is ruby. mixing in something here and something there is allowed, You are brilliant with the new concern stuff.
so the only thing ii am concerned about this, is the word “concern” it self.
why choose a word, which carries a low meta-physical frequency to deal with anything or something, while knowing that could get one in trouble? :)
concern in the noun form means: anxiety; worry.
ii know how hard it is to choose a proper word for something new (or old in a new dress, as in this case). choosing a word which would feel balanced and would be able to describe everything programmers are worried and fear about their “fat” objects.
ii know You should replace all concerns with regards.
ii know this, because ii know You designed the idea of “concerns” to deal with Your actual real-life worries and anxiety about Your “fat” objects. You felt concerned, and that is why You choose that word.
but ii am here to give You back, what You gave me in the first place, when You created the framework itself. You gave me balance, ii saw a balanced man, doing balanced things. as ii learned ruby and Your framework ii became a dad, just like You did recently!
so ii am here to tell You, there is no need to be concerned, You should not be concerned, worried or anything. You should be balanced, and advertise joy, like You did show us 8+ years ago…
regard in the noun form means: Attention to or concern for something.
in the end it is about attention to a detail. the word “regards” is also used as a greeting, so it does have a high meta-physical frequency, positive, life-giving. and it still means the same thing, to worry, but without the low freq.!
would it not feel so much more balanced to the human experience, if one would be mixing in:
ActiveSupport::Regards
and think about models regarding Tagging, Commenting…
ii could then say, ii coded up some code regarding the tagging of the posts, and it would be no problem to find app/regards …
if You do it, and rename it, You will see an amazing change in the feedback You get from the community, ii know. You would be sending out amazing ActiveSupport::Regards to all rails programmers :)
anyway, thank You!
enjoy, Miha Plohl kitschmaster.com
greg
on 22 Dec 12Object oriented programming inquisitors attack!
I hope that pragmatic programmers are strong enough to defend Ruby on Rails.
That discussion explains a bit how in Java world, EBJ and other non usable APIs were created. It is easy to lose the debate with inquisitors when taking into account only extreme cases.
Linus once said “talk is cheap, show me the code”. When Robert C. Martin was asked for DCI example applications, he answered that he has one but cannot show it. I really would like to see an average DCI application build by mediocre programmers. Its ease of modification and build cost would prove if DCI is a good approach or not.
This discussion is closed.