Explicitly-Named Classes with Explicitly-Named Methods
Learn about explicitly named classes and methods in our Rails application.
When implementing the business logic, there are a lot of design decisions that need to be made. The architecture of our app serves to tell us how to make some of those decisions. Not putting our business logic in an Active Record is a start. We can eliminate even more design decisions by creating conventions around this seam between our logic and the Rails-managed outside world.
What is the simplest thing we can do (besides putting our code directly in Object
)? If we had no Rails, no framework, and no libraries, we’d need to make a class with a method on it and call that method. Suppose this is our strategy for business logic. Suppose we always put new code in a new class and/or a new method. This would eliminate a lot of design decisions.
It turns out this strategy has further advantages beyond eliminating design decisions. First, it doesn’t require changing any existing code, which reduces the chances of us breaking something. Second, it provides a ton of flexibility to respond to change in the future. It’s much easier to combine disparate bits of code that turn out to be related than it is to excise unrelated code inside a large, rich class.
Classes like this are often called services, and we encourage the use of this term. It’s specific enough to avoid conflating with models, databases, data structures, controllers, or mailers but general enough to allow the code to meet whatever needs it may have.
So, what do we call these services?
A ThingDoer
class with a do_thing
method is fine
Barring extenuating circumstances, we’ll choose a noun for the class name and make it as specific and explicit as possible to what we are implementing in the context of the domain and app at that time. This means that early on, the names are broad, like WidgetsCreator
. Later, when our domain and app are more complex, we may need more explicit names like PromotionalWidgetsCreator
.
The method name is a verb representing whatever process or use case is being implemented, which will create some redundancy. For example, create_widget
. We might be feeling a bit uncomfortable right now because we are no doubt envisioning enterprisey code like this:
WidgetsCreator.new.create_widget(...)
What we are suggesting will definitely result in code like this. We won’t claim this code is elegant, but it does have the virtue of being pretty hard to misinterpret. It also closes the fewest doors to changes in the future.
Now, we might think we have a Widget
class that has a create
method. Isn’t that where widget creation should go? We understand this line of thinking, but remember, a Widget
is a class that manipulates a database table that holds one particular representation of a real-life widget. The create
method is one way out of many to insert rows into that table. There is no reason to conflate inserting database rows with the business process of widget creation.
What if we require another way to create a widget? The WidgetsCreator
class can grow a new method, or we can make a whole new class to encapsulate that process. We can couple these implementations only as tightly as the underlying process in the real world is coupled. Our code can reflect reality. Wrapping it around the insertion of a row in a database divorces our code from reality.
We might be thinking we should not have to call new
, or perhaps create_widget
should be named in a more generic way, like call
. We’ll get to that, but let’s talk about input to this method first.
Methods receive context and data on which to operate, not services to delegate to
There are typically three types of objects we need access to in order to implement our business logic in a Rails app:
- Rails-managed classes like our Active Record classes, jobs, or mailers.
- Data-holding objects (Active Records or Active Models), which are typically what is being operated on or a context in which an operation must occur.
- Other services needed by our service to which we delegate some responsibility.
A significant design decision—after naming our class and method—is how our method’s code will get access to these objects.
Rails-managed classes
In the vein of facing reality and treating things as they are—not how we might like them to be—we are writing a Rails app. Rails provides jobs, mailers, and Active Records. Using them directly—thus creating a hard dependency—is fine. We are likely not (or shouldn’t be) writing code to work in any Ruby web framework. Further, unless our code needs to be agnostic of mailer, model, or job, there’s no value in abstracting the actual implementation. The class needs what it needs, and we should be explicit about that.
Data-holding objects
Our method exists to operate on data or perform a process in the context of data, and this data should be passed to the method directly. This information is not specific to the logic but what the logic exists to operate on or within. For example, if Pat edits a widget, the logic is the same as if Chris edited a different widget. So we’d pass an instance of User
and an instance of Widget
to our method.
Other services
Other services, whether services we create or third-party classes we’ve added to our app, should either be referred to directly— if callers should not configure them or specify them—or passed into the constructor—if the caller must configure or specify them. Note the distinction. If the logic requires a specific implementation, it should be strongly dependent on that. If it’s not, it shouldn’t be. Making all dependencies generic and injectable belies the way the logic will actually work.
When we follow these guidelines, our code will communicate clearly how it works and what its requirements are. For example:
Get hands-on with 1200+ tech skills courses.