jose.omg.lol

It’s none of their business

Rails lends itself well to object-oriented programming — better than any other paradigm. As such, Rails codebases are at their best when they embrace OOP.

Now, creating classes that encapsulate data and behavior is technically OOP. But by itself it’s not a very good strategy. Ideally, among other things, we should aim to create a rich domain model (i.e. classes that succinctly encapsulate a problem domain).

Let’s consider a domain example in the context of a blog (these are concepts, not necessarily classes) —

A Publication::NotificationJob is a subclass of ActiveJob::Base. So it has inherited the Job suffix from its ancestor.

In Active Job’s context, a Job is an abstraction that allows callers to optionally defer execution. But what actually is a Publication::NotificationJob?

We could say "it’s a class that’s responsible for notifying about a Post being published". But where does the deferred execution part come in? Its suffix says Job, so clearly that’s part of it. Why weren’t we compelled to include that?

That’s because what we think we’re modeling isn’t what we’re actually modeling.

Our familiarity with Active Jobs and how they’re often written allows us to make the conceptual jump — this class contains logic for sending notifications, with the implicit characteristic that it may be deferred thanks to subclassing.

That tacked-on detail is a smell. Inheritance is a mechanism meant for "is-a" relationships. This class is two things. Namely, an Active Job, and also a "class responsible for notifying about a Post being published".

What’s the problem, anyway?

Broadly, using inheritance to share code (in this case the deferred execution characteristics) can be problematic. But that’s not the issue here. As we most likely won’t be subclassing further, which is when the problems compound.

No, the real problem is that we’ve cheated ourselves out of a richer domain model.

We’ve opted out of an object-oriented domain, which means we get none of the benefits of OOP. These are too many and too nuanced to cover as part of this post. But, generally, the mechanisms available to extend and share behavior are less flexible now.

Let’s say our blog adds a paid tier and wants to notify subscribers about deals for signing up. We’ll want to share the notification part of Publication::NotificationJob, but not the publication part.

The issue isn’t whether we can make a procedural system work by stuffing logic into the job. It’s about how direct and simple making the change is. In changing the procedural job, you might picture yourself cutting and pasting code into a new class that’d be shared between Publication::NotificationJob and Promotion::NotificationJob — simple enough, right?

Is it simpler than adding a single method or argument, though?

To borrow from Sandi Metz's excellent book: "Design is the art of preserving changeability". My argument is that moving the logic into a domain object like Notification, which the Job simply calls, makes the code more changeable than dumping everything into the job itself.

To be continued

I’m aware an example or two would help cement these ideas. I’ve decided against including those to leave this a bit open-ended.

This post is the beginning of a new series called "Object-Oriented Rails" and I’d like to leave plenty of room for exploring these concepts in future posts. In the meantime, let me know which parts you’d like me to expand on next time. Check out my homepage for links to my socials. Thanks for reading!