Helping devs understand concerns faster

Looks like the preferred home is The Getting Real Basecamp YouTube channel.

4 Likes

On the other side, I’ve been on Rails for about the same duration, and I couldn’t imagine my life without them. I will break out into concerning :X do; end at the snap of fingers.

There really seems to be an information gap here
as there is one with Git :eyes:

1 Like

Hah, I thought this thread was about ActiveSupport::Concern

TIL about Module::Concerning

2 Likes

I was thinking of ActiveSupport::Concern but Module::Concerning is also part of this too.

Well, yes, Module::concerning does create a module extending ActiveSupport::Concern.

They’re all concerns, which are basically just fancy modules.

IMO, the problem with concerns, is that they are really nothing special, they are just plain Ruby modules. But the fact that a new word was introduced creates an expectation that you’re missing something. Which is something I totally understand.

A concern is a module that you extract to split the implementation of a class or module in coherent chunks, instead of having one big class body. The API surface is the same one, they just help organize the code. A concern is a regular Ruby mixin, there’s nothing else, it is not a new concept. It is plain Ruby.

So, when you extract pricing-related API from a Hotel class, you typically do something like this:

# app/models/hotel.rb
class Hotel
  include Pricing
end

# app/models/hotel/pricing.rb
module Hotel::Pricing
  # Hotel pricing-related logic.
end

There, Hotel::Pricing is a concern. But that idiom is not Rails-specific, this is vanilla Ruby programming.

Directories like app/models/concerns are meant for mixins that can be reused, in this case, in the model layer.

On the other hand, AS::Concern is orthogonal. A concern is a Ruby module used in a certain way (vs used as a namespace, say). Ruby has the included callback that gets fired when a module is included somewhere, and you receive the enclosing class or module as argument, that allows you to decorate it, for example. Well, the included DSL macro is essentially the same thing, except that the decoration looks prettier. It is a DSL for mixins that want to decorate their enclosing classes or modules.

But, perhaps surprisingly, a mixin does not need to extend AS::Concern to be a concern. Or to call concerning. A concern is a Ruby module with a certain purpose, as explained above, and whether you use those DSLs or not is irrelevant.

And you see the problem, concerns are so simple that they do not deserve a full guide. Concerns are mixins, if you are a Ruby programmer, you already know what a mixin is and their use case to modularize APIs.

But at the same time, a post like this is needed to explain that they are just that!

That means there is something to address here, I believe we are missing a definition, but don’t know exactly how to fix it.

6 Likes

I think there’s a lot of Rails users that don’t know this. I think there’s a decent number of Ruby devs who know vaguely about mixins but they don’t use it because they don’t understand it. If you combine teaching folks how to use and create concerns properly and documenting the concerns that are already in Rails that folks can use, I think there might be enough for a guide.

I still think there’s value in a generator or something sort of assistance for helping people use concerns in a consistent way with a convention.

1 Like

Maybe, but you have to draw the line somewhere, in my view it is out of the scope of Rails documentation to teach you Ruby. As a Ruby programmer, you need to learn Ruby.

As I said above, I believe there’s something to address, this thread is a proof of that, and I’ve seen others like this. But I don’t know how.

Regarding a generator, which usage have you in mind? How does the command look like? What does it generate?

I mean, if we’re here to talk about getting folks up to speed on using Rails properly I don’t think this is a great attitude to have. I get the concern (no pun intended) but I wouldn’t exclude it as something the community should evaluate.

My first though is something like this: rails generate concern name_of_concern type_of_concern --class_methods class_method_names --included_methods included_method_names where type_of_concern is model/controller/job/mailer/etc

This would:

  • Place them into the app/type_of_concern/concerns directory
  • Create a test for the concern which includes stubbed tests for each of the class methods and each of the included methods. It would also include a stubbed version of the model/controller/job/etc for including and testing the concern.

The dependency-resolution between modules goes beyond mere syntactic sugar, IMO.

A concern does not have syntatic sugar per se, like the Hotel::Pricing module above. The syntatic sugar is a feature of AS::Concern, different.

@Adam_Lassek @fxn When I read the tone of your comments it seems like you’re arguing. When I read the content of your comments it seems like you’re loudly agreeing with each other (but maybe not realizing it because you’re each grabbing at a different part of the elephant.)

I’d like to summarize what I think you’re both saying, so that the “argument tone” doesn’t wind up confusing newbies who might struggle a little more to understand the content.

The “Concern” pattern is another name for using Ruby modules as mixins. There’s nothing special about concerns when they’re done as PORMs.

However, when a module is created using concerning or by extending ActiveSupport::Concern, there’s some special dependency resolution logic that happens that is Rails-specific.

Does this feel like an accurate summary?

3 Likes

There is no argument, Betsy.

This is a technical conversation about a technical topic.

My experience is that it is often difficult, especially on the internet where tone doesn’t convey, to tell the difference between finely grained technical corrections and arguments. See also the Social rules - Recurse Center section on “well, actuallys.”

I’d like us to be extremely careful about this within this forum, because it’s intended as a welcoming space for people to bring things that confuse them. If they feel well-actuallyed, particularly by a core team member, that shuts conversation down and we lose the data about the Rails experience that they would otherwise bring to the table.

My summary was intended as a way to add context to your discussion. If a newbie googles about concerns, then a topic on the official Rails forum called “helping devs understand concerns faster” is going to have a lot of SEO juice. I’d like any newbies reading this to come away with the correct technical impression, and I was afraid that that was getting lost within the back-and-forth.

7 Likes

All that said, I fundamentally agree with @fxn’s point that PORM mixins are a Ruby basic that Rails developers need to learn about to be proficient.

I think the binary between “no help” and “smothering levels of handholding” is a false one though. Maybe we can find some solution in between?

I think the Rust compiler threads a similar needle well. Its errors are extremely helpful in guiding you away from stupid pointer mistakes. However, in order to fix those mistakes a developer will need to learn to understand pointers, the stack, and the heap; and the Rust compiler does not pretend otherwise.

I don’t think an error-oriented approach is the right one here, I think “I don’t understand what this is for” demands something more proactive. I have no idea what, but I’m hoping someone else does.

2 Likes

Yes, concerns are just modules, yet they’re also more than that. They’re also about a style of project organization. So they’re really doing a lot of work, and no wonder it’s confusing! And just like it’s not always obvious that app/models can contain POROs or other non-db backed models, it’s not obvious that app/models/concerns isn’t really where you’d put most concerns.

I’m extracting from DHHs screencast here.

We have an Active Record model that includes a concern, here expressed as just a module, which wraps a PORO for a nicer call API. All namespaced within User:: and app/models/user. This is how the vast majority of models at Basecamp look, for instance.

Note: this is about the organization, not so much the content within, in the real app there’d be more code in these pieces.

# app/models/user.rb
class User < ApplicationRecord
  include Notifiee
end

# app/models/user/notifiee.rb
module User::Notifiee
  # Philosophically a concern, i.e. a role a model can play, but:
  # extend ActiveSupport::Concern is not needed without included/class_methods calls.

  def notifications
    @notifications ||= User::Notifications.new(self)
  end
end

# app/models/user/notifications.rb
class User::Notifications
  attr_reader :user
  delegate :person, to: :user

  def initialize(user)
    @user = user
  end
end

I totally wish we had some more documentation or tooling that could push you in this direction or make it more obvious how this could work — perhaps in guides or generators.

9 Likes

I think we’d do well to explain concerns better. I’ve been writing code with mixins for so long that I’ve taken it completely for granted that this is a wonderful way to structure separate domain concerns.

But I also agree that concerns do not warrant a generator. A lot of concerns are simply Ruby modules that are mixed in, with no additional stuff added. And then for a few cases, like when your concern includes both class and instance methods, or you want to call class methods on mixin, you can use extend ActiveSupport::Concern.

I’d also just put my two cents here that I’m actually not that big a fan of Concerning. That is adding inline concerns in a class. We don’t use that at Basecamp any more. It was an approach introduced by @bitsweat, so I’ll let him speak to what he thinks about it.

But I went over the way we use concerns at Basecamp at length in that video that was linked above. Here’s another example from HEY:

class Identity < ApplicationRecord
  include Accessor, Authenticable, Boxes, ClearancePreapproval, Clipper, Creator, Eventable,
    Examiner, Filer, Member, PasswordReset, Poster, Reaching, Regional, TopicMerger
end

module Identity::Accessor
  def accessible_topics
    Topic.joins(:accesses).where(accesses: { contact: contacts })
  end

  def accessible_entries
    Entry.joins(:topic).merge(accessible_topics)
  end
end

As you can see, we often put as little as 1-2 methods into a concern that encapsulates that specific concern. The way you reach accessible objects. These objects absolutely could just have been declared in Identity, but add all those concerns up we have in Identity, and you’ll see that Identity would be a big basket of dozens of methods. Sure, all these methods are about Identity, but there’s a grouping below that. That’s what concerns are: A way to group related methods together, while still living under a single class.

You can think of it like this: The class is an H1, the concern is a H2, the public method is an H3, and private methods could play H4/p. It’s a way to structure a large argument. Sit down, and lemme tell you about the Identity class! Let’s start by talking about how the Identity acts as an Accessor, and you should reach what that identity can see as an accessor through the accessible_* methods declared in the Accessor role.

It’s structured writing.

18 Likes

Worth pointing out here is that the stuff we put in app/models/concerns are concerns that apply to multiple classes. An example from HEY is eventable:

module Eventable
  extend ActiveSupport::Concern

  included do
    has_many :events, as: :eventable, dependent: :destroy
...

class Access < ApplicationRecord
  include Eventable

class Clip < ApplicationRecord
  include Eventable

But the majority of our concerns are actually like those presented for the identity: Just a way of breaking up a large argument, like Identity, into smaller ones, like Accessor.

Now I understand that argument from people who say: well, Accessor should just be it’s own class! Yes, it could have been. You could do Accessor.new(identity).accessible_topics. But I find that style awkward. The fact that the identity is playing the role of an accessor is implicit in #accessible_topics.

So that’s another way of putting this: It’s a writing style. Like using subheads to explain subservient ideas within a broader context. You could probably extract all those subheads out, and turn them into little essays of their own. But that’s often just not the right level of extraction. As you saw, the Accessor role starts life as literally just two methods! It doesn’t warrant being turned into it’s own class. It doesn’t have an independent center of gravity.

As you’ll probably gather from all the writing metaphors, this isn’t a technical debate in the sense of “When x, then do y”. It’s a discussion of writing styles. Subtle breaking points. Careful evaluations.

But at the same time, you can absolutely teach style. A Rails Strunk & White would be great :smile:

20 Likes

I think that we can do more work in the guides on the getting started section. Current getting started demo app is ok but it is extremely bare-bones that just shows how easy is to start with Rails. We could expand a little(not too much) on that app and show how concerns can be used and other stuff too.

We could expand that section to introduce:

  • Sessions - login/logout
  • Refactoring with concerns
  • Tests and Fixtures
  • A dash more of Turbolinks and webpack
  • Maybe also add Mailers and background jobs
  • Maybe even deployment

I had a pleasure in my life to coach a few young devs that were new to Rails. I couldn’t accomplish a good introduction to Rails with the guides, we needed to use other paid courses/resources just for a basic introduction. But, if we expand a bit on the getting started demo app I feel that can be the best starting point.

6 Likes

When you said this, it made me think of the Securing Rails Applications guide. Not quite the same but something to get to some inspiration from perhaps.

1 Like