At the end of the day concerns are just Ruby modules and they are a core feature of the language. They are an acceptable style of programming. It all depends on the task, your team's size/maturity and the hard boundaries you'd like to enforce.
The problem with concerns is that they can easily start leaking logic into other concerns or models and you've only got your team's conventions and common sense (which are all very soft boundaries) to steer you away from this. As a matter of fact, same goes for Rails engines - they make it very easy to call the parent app from within an engine and it's very tempting to do so at the cost of leaking logic and breaking these boundaries.
If your team can agree and stick to a set of conventions in regards to concerns, there's nothing wrong with using concerns the way Basecamp uses them. If you prefer stricter boundaries, there are other patterns that you can follow (like service objects or ActiveJob or events).
I personally use concerns when the behaviour tends to be very generic ("Paginates", "Cacheable", "SoftDeletes") and service objects for anything that touches the business logic.
I've been doing Rails development for a decade. Concerns are probably the biggest code smell/anti-pattern I've ever seen in any application. It's used as a bandaid to "break up" classes which do too much (but you really don't, you're just hiding the complexity), or it's way over done and everything is magic and almost incomprehensible and unmaintainable. Overall, just a total nightmare to deal with. Would not recommend, and I always try to steer people away from using concers during code reviews.
Don't put the logic in activerecord models at all. Not even the data.
Use activerecord uniquely as a querying mechanism (read or write), don't use relationships and don't put validations in there.
Create objects (aka behavioral objects, aka servoce objects) for the logic and create entities (plain ruby objects) when you need to pass around the data.
Yes, you are essentially eliminating the entirety of activerecord.
After 10 years of rails, you realize that is the only safe way to use that library.
By the way
User
User::SignUp
Are related. The behavior doesn't need to be in the same object, the namespace takes care of that already.
It's hard to answer, in the sense that yes, you can use ActiveRecord relationships for the purpose of _building a query_. The point is, you should use them only for that. And it comes with the downsides of distancing you from SQL, which is not negligible.
The problem is if you try to access relationships that you didn't load from the object, that shouldn't be done.
It's the autoloading from the database that is the real problem.
If you have enough discipline and enough authority to ensure that never happens within the software you are working on, you can.
It's really culture problem. As soon as you pass around a `user`, someone will type `.posts` on it, or `.save`, suddenly your business logic depends on the database shape, rather than on contracts.
What if your database shape is wrong? What if your database shape needs to change?
The goal of good architecture is to be resilient to change, or to even _postpone the choice_ to a moment in time where you have more data to make the right one.
The safest way is: query with activerecord, map to plain ruby objects and discard the activerecord ones immediately.
This will also help you discover the entities you didn't know about, for example, if you have a table of users identified by email, that's the "User" table in Rails. However, let's say that you take only a subset of that table (with select), such as "Full Name" and "Email": this could represent something different. A Newsletter::Subscriber for example.
And the newsletter subscriber entity can be used in the Newsletter::Weekly::Send object, as well as the Newsletter::Unsubscribe.
If you realize that this should not have been in the users table in the first place (I have no idea myself), the cost of change is way lower than if you passed an activerecord object around.
I hope this answers some of your questions. There is a lot to say on the topic.
I'm happy to chat more on slack or an online videoconference session
Thank you for taking the time to answer, and also to amw-zero below.
I think I understand more of the sentiment now.
> As soon as you pass around a `user`, someone will type `.posts` on it, or `.save`
I'm not much of a fan of the active record pattern, and I can see (have seen) how optimistic/naive developers can get in trouble - but I feel these are two separate problems:
> someone will type `.posts` on it
This doesn't have to be all bad, just make sure there was a join or include first - perhaps via a scope (eg: User.with_posts).
I suppose some rails apps end up doing "much more than crud", and then it can be easy to stumble. But I find that with some modicum of discipline, "fat models" can go a long way towards making "slim controllers" fall out naturally.
> someone will type.. `.save`, on it
This why I use AR objects, so I can do crud. Sure if dealing with "posts" (plural) you want to avoid: posts.map... save in favour of update_all.
And while there's save! - data integrity belongs in db constraints. But validations are a great tool for better ux and error feedback. The major problem today, is probably that you'd want to (only) run them as client side js.
I did delete this post, I'm not sure how it came back. I wasn't happy with my answer.
I would avoid any logic in the models, they already have 3 responsibilities (data gateway, data holder and validation).
For CRUD apps, Rails has competitors that can't be overlooked, there are alternatives: PostgREST, Hasura + Forst Admin provide full crud UI with borderline no code.
In all honesty, you don't have to look far. Elixir + Phoenix, which is not free of mistakes, manages to be a better Rails than Rails itself. The validation is isolated, the database access is isolated from the model, no autoloading is involved.
That is very interesting approach! I’ve never worked on a project that doesn’t use AR relationships e.g has_many...
at all.
Do you have an example app or a blog post exposing this pattern? It would be very interesting to see how this actually works. Thanks you very much for bringing this up!
They aren’t saying to not use has_many. They’re saying to not use the association method that gets put on the AR object from has_many. For example, if a User has_many Posts, they’re saying to avoid calling user.posts.
That may seem weird, but the association still has value: for querying. You can still write: User.joins(:posts) for example.
The problem with the association methods is that they aren’t really methods because they always execute a query. A lot of longtime Rails developers get tired of the database being involved in every step of the way in a request, because it leads to a lack of separation of concerns.
Unfortunately I don't. I work less and less on the CRUD part of the apps, however as I recommended in another comment, Phoenix (Elixir's alternative to Rails) does that correctly: associations are used for the purpose of querying, not as methods.
In combination with isolated validations and the data gateway isolated, it's easy to achieve separation of concerns and have a more maintainable code-base.
Good idea. The lazy loading has been particularly bad in the code-base I worked on recently, to the point that pieces of code thought to be entirely isolated from the database were actually accessing the entirety of it.
Callbacks to me are just an indicator of what is missing, an object that encapsulates the "user case". There is a need to do "other things once the data is written", but that is specific to the location where the writing is happening. Modifying a post content from an administrator's perspective could trigger an email to inform the user "your post has been moderated", but the user modifying it's own post shouldn't. Yes you can have callbacks with IFs, but the use-cases will keep piling up.
Now you have a rake task that fixes some posts' content due to a bug in the whatever part of the code parsed the text. It shouldnt' trigger the email, but you do have a callback in there.
I do my best, but the sad reality is that I made an insanely large amount of mistakes in my development life, I didn't reach these conclusions without doing damage myself.
I try to give back by training developers and making sure the business gain awareness so they don't have to experience the complete halt of feature development when the software becomes unsuited for changes.
Not arguing your points, just a wondering: Why stick to Rails then? Without ActiveRecord Rails is a pretty big bloat with a mediocre view layer. Routing is nice but I'm sure Sinatra could do that as well.
The majority of Ruby jobs is on Rails.
All I need, personally, is a server like Puma, a router like Roda and the pg gem really. Then of course it depends on the application you need to develop.
I would probably need some library to coerce string values to types (boolean, integer) for forms, and a mapping mechanism of some form to convert a pg result in a data shape I control.
If you are truly sharing logic then it might be okay, but typically I've seen concerns added as a way to organize code when ActiveRecord models get too big. At this point a lot of people reach for Concerns because it is right there in Rails waiting to be used, but ultimately it's just papering over the problem of bloated classes with too many responsibilities. I usually prefer to add some kind of service layer to handle complex manipulation and object lifecycle so that ActiveRecord just manages querying/persistence with most if not all business logic at a higher layer.
> it's just papering over the problem of bloated classes with too many responsibilities.
If you're blindly coding away and using some feature just because it's there you've got bigger problems with how you go about programming, that's not special to Rails.
I find concerns to be an intermediate mechanism to use in migrating the bloat out to some other abstract level. However, adding abstractions beyond MCV has it's own problems, each level means the developer has to understanding something more, one can't escape that.
Pretending that one can fully anticipate the complexity of large project and model it perfectly via the way you organize your code is really only something done by the the beginning programmer. Concerns can be very tight, as you note, and of course any feature can be used poorly. I like concerns, we have many that haven't been touched, and lead to an ability to extremely rapidly spread functionality around a huge number of models (e.g. different annotation classes). I'm also guilty of using them in the intermediate manner above. In the long term, one sees these patterns, and doesn't fret about such things.
I think a different way of looking at the problem is that ActiveRecord models really shouldn’t have much, if any logic. Instead, logic should get delegated to other types of object, such as services, decorators, query objects, etc. While “fat models” is certainly better than “fat controllers”, I’m afraid it lead the Rails community astray with large-scale apps that required more nuanced architecture.
Agreed. Concerns break large classes up to an innumerable amount of small files. It becomes so hard to keep track of simple interactions because they are spread across half a dozen files and/or contexts.
I empathize with everything you're saying here, but I do humbly disagree. I've experienced exactly what you're talking about, and it's a nightmare. Frankly, I think the Rails codebase itself even suffers from an over use of concerns that can make figuring out where functionality comes from very frustrating at times.
That said, concerns are mixins, and mixins are, in the simplest of terms, more or less multiple inheritance. That's powerful stuff, and powerful stuff in the wrong hands (which includes every hand that'll touch it, not just the author!) will always get you in trouble. That could be the motto for Rails itself - powerful stuff, but in the wrong hands it'll get you into trouble!
My first exposure to concerns in Rails was a awful codebase for a well known company with massive classes that someone went on a weekend bender and shrunk down to less massive classes by introducing dozens of concerns. It made the classes _appear_ smaller, but they weren't actually. In reality, it was just hiding away much of the complexity and horrible decision making that made the code so hard to reason about and maintain and took things from bad to worse as a result.
That said, when you have a well architected code base to begin with (as in before you start introducing concerns) that has svelte classes with appropriate responsibilities, with shared logic broken out into "services" or whatnot, concerns can be a really nice way to share isolated functionality that can lead to cleaner classes without obfuscating what's going on. Quite the opposite - used well, they can tuck away (but not altogether hide) ancillary functionality allowing the meat of a method to be highlighted instead of drowned out by pre- and post-requisite behavior that might take up more LoC than the unique business logic of a given controller action thus obfuscating the functionality specific to that method.
On my current and previous project, concerns worked out great for the teams (which included junior devs) but were used sparingly and typically introduced by experienced devs. Also, taking a quick glance, it seems like Controller concerns are favored far more, and often involve calls to class-level ActionController methods such as rescue_from, overriding protect_from_forgery defaults, settings layouts, before_action, helper_method, etc. - things that you can't just tuck into a service class or method that gets called from the controller and would otherwise require someone remembering the correct combinations of code to sprinkle into a controller or creating a controller to inherit from (which isn't practically different from concerns but can paint you into a corner). They're also never used for the business logic of an action, only supporting functionality (again, pre- and post- requisite functionality or introducing class-level helper methods that can be reused).
All that being said, for a team that isn't confident in how to use concerns effectively, I'd also steer them away and guide them to consider alternative approaches. But at face value, I don't consider concerns an anti-pattern and have found that, when used appropriately, they can be a great tool in maintaining a clean codebase.
Forgive the slight tangent but I'd like to talk about Rails in general:
Just last night I was migrating an app from Rails 4 to 6.1 - I have been using rails since 0.9, but I think my long relationship is coming to an end.
What I loved about Rails was that it built a web framework with the same basic idea as Ruby: optimize for developer happiness. Trying to use the webpack integration (released with Rails 5.1 I think) is a nightmare. Suddenly my clean Ruby world is polluted by JavaScript package hell, there are now two parallel asset pipelines with subtly different behavior, I can no longer reference JavaScript directly in my views, and the convention over configuration Rails approach seems to have been superseded by a tool with the most nightmarish proliferation of necessary configs that I've ever seen.
To top this all off, none of this is documented well in any of the official Rails resources. The Rails JavaScript guide mostly focuses on their rails-UJS tool (which is fine, though also lacking in actual API documentation) and makes no mention of any of the webpacker stuff. The only docs I could find were in half finished pull requests in forked repositories, and some brief notes in markdown files.
Before starting with Rails 6, I was excited to try out the new Stimulus.js, and hopefully the improvements derived from Hey.com. Now I want to tear out every bit of JavaScript integration from the framework and manage those assets entirely on my own.
Yeah migrating my Rails 5 system to Webpacker from the pipeline was not smooth and you're right, Rails ended up adopting a system that is configuration hell.
All that said - I love Stimulus.js and Webpacker now that I fought through all the insanity (Webpack configs are nightmare of splicing and trying to reference relative folders either in node_modules or some alias located... well webpack doesn't know...)
If you're on the fence, migrate the entire thing to Webpack - forcefully, with prejudice. Living in both assets pipeline and webpacker is a mental duality that don't co-mingle.
By forcefully, I mean copy/paste raw assets instead of relying on their package that sticks it into the old pipeline.
Stimulus is nutso lightweight and "just makes sense". Everything is self contained.
I had a similar nightmarish experience migrating a few Rails apps from 4.x to 5.x. Because of the complexity and size of our apps, it took a few months to successfully complete and test. Luckily we had copious rspec tests with decent code coverage. I too dislike the troubling trend of not being able to directly access JavaScrips directly from the view; I am not sure why this is done, but it makes debugging much more difficult.
More abstraction is not more better.
In any case, we will eventually have to migrate to 6.x, so that will be fun.
I have two applications in the wild— one a monorepo with a rails API and front end built with deploy scripts, and one with rails Webpacker. The Webpacker one feels like I have an arm tied behind my back. There’s a bunch of ‘Rails way’ extras I have to do to keep things ‘organized’, but I don’t even get proper state-maintaining hot reloading in dev. It does smooth over some rough edges (though through magic) but the next one will probably prefer the self-management strategy.
I spent a ton of time writing custom asset management in rails 2.x days. It was kind of fun. I think you can still use rails 3/4 style assists in rails 6?
I haven't had to upgrade to 6, so it's a bummer to hear it's so painful. On a fresh Rails 6 project built with "--webpack=stimulus", I haven't had to muck with Webpack (on two ongoing products and a handful of experimental/toy codebases) and Stimulus has been just a delight. Everything more or less "just works".
Webpacker docs (or even in general) is a bit painful I agree. I don't think Rails had a lot of options there, chasing the constantly involving js world was hard (Sprockets is very limited in what it can do).
I think the docs and best practices will improve but yeah I feel your pain.
That was my experience as well. Usually upgrading years old codebase to the bleeding edge took way less than expected. Webpack on the other hand is a different beast to handle and a challenge in itself.
Concerns are the best example of the flaws in Rails and Ruby. The fact that DHH came up with a mixin applied at runtime as the solution to organizing Rails apps is frustrating. Other programming ecosystems have rightly moved away from mixins and do-everything-dynamically, along with moving away from mixing data and methods that self-mutate that data.
Having worked on large real world Rails apps, concerns are indeed a poor pattern in practice. They introduce hidden, untraceable dependencies, can clobber each other silently, make it difficult or impossible to understand where methods come from, and bloat classes into super coordinators, when "models" should only be data. Combine that with Ruby's horrible preference for metaprogramming and you get unmanageable code.
PS: If you work at a company as big as Github that can afford to literally employ Ruby core maintainers, then you are welcome to reply with "but it works for Github!"
"Combine that with Ruby's horrible preference for metaprogramming and you get unmanageable code":
Is it impossible to write maintainable code in Ruby or is it just teams of people with varying skill levels working under pressure and hard deadlines?
Do you think the average Rails app is less maintainable than the average Node / PHP or even Java app? I'm not so sure.
It's teams of people with varying skill levels under time pressure.
Ruby is a language where projects can suddenly bloom with hyper-complexity if you're not careful. You really have to strictly adhere to a style guide and have seniors who can catch the violations automated checkers like Rubocop can't.
It might be a bit more pronounced in Ruby, but I doubt this doesn't happen a lot in javascript / python / whatever language actually.
You always better have at least a couple of seniors in each team that go over every PR and maintain the project's health. No getting around that no matter the language.
Now languages like php / ruby / js are notorious for their low code quality but that's less about the language and more about how and where they're used: tons of startups, young people in the beginning of their careers, pressure to find market fit etc. I'd be surprised if .net / java / python projects don't turn into crap under these conditions. The worst code I've produced was written like that and I really doubt the tech stack would have made any big difference in the quality of code I produced.
I've been working on Ruby apps again lately after spending a few years in the iOS world. I don't miss iOS at all but moving back to a slow, dynamically typed language like Ruby after getting used to Swift is like trading in a Tesla for a Model T. Ruby was great it its day but I think we can do better now. Maintaining large Ruby codebases is no fun.
No fun for you, maybe. And I can see where you’re coming from. I often find it very fun, though, which has prevented me from porting an API to another language like Go or (gasp) Swift. Ruby gives a lot of flexibility, and in my line of work (especially with covid mitigations) there are lots of immediate change requirements; flexibility is very important. That flexibility provides the ability to shoot yourself in the foot (and I’ve surely done it), but it also allows us to pull off events that have suddenly changed with little warning. I like that.
Not really sure what's Ruby's "slowness" has to do with anything? Were you having performance problems with Ruby or did you just throw in it being "slow" to prove a point?
I worked on quite a few Rails apps for 6+ years and Ruby's speed was never an issue.
I tend to only use concerns in extremely cut-and-dry cases. And even then, I sometimes get annoyed with them when hunting for that one method which is tucked away in a concern somewhere. But sometimes they are the best solution for making sure a bit of shared behavior doesn't get out of sync between models.
I'm an old dog. If I'm honest, Rails is the only thing I know (... I barely know a lot of things).
Counter do DHH's and core Rails' pattern, I'm thin models and Service Objects (GASP the horror!) to handle business logic. Models become DAO-y.
I had a lot of mental trouble handling different use cases for the same object. Like if an Admin vs User updates a post. The notification chains are completely different. So now my service objects look like: Post::UpdateByAdmin < Post::Base vs Post::UpdateByUser < Post::Base.
So, in general, my concerns are pretty thin. Like the post only handle data grabs. Even then, I'm guilty of doing what the post says from time to time especially when I couldn't see far enough into the future.
I've never really had to program using Ruby but just reading this Concerns concept makes me shudder. Am glad that I do not have to deal with the code structured in such way.
The problem with concerns is that they can easily start leaking logic into other concerns or models and you've only got your team's conventions and common sense (which are all very soft boundaries) to steer you away from this. As a matter of fact, same goes for Rails engines - they make it very easy to call the parent app from within an engine and it's very tempting to do so at the cost of leaking logic and breaking these boundaries.
If your team can agree and stick to a set of conventions in regards to concerns, there's nothing wrong with using concerns the way Basecamp uses them. If you prefer stricter boundaries, there are other patterns that you can follow (like service objects or ActiveJob or events).
I personally use concerns when the behaviour tends to be very generic ("Paginates", "Cacheable", "SoftDeletes") and service objects for anything that touches the business logic.