Proper location for null objects
I've recently watched couple of screencasts on using Null Object Pattern: https://www.rubytapas.com/2017/01/31/two-screencasts-two-ways-eradicate-ruby-nil-values It was a good reminder about a question I've already been thinking a while ago. What could be a reasonable location within a project source tree to keep null object definitions?
Let's follow the situation from the first screencast, assuming we have a typical Rails project structure for determinancy. There is
User model and related null objects called
GuestUser. Should it be
app/models/guest_user.rb? Or may be it is better to keep both classes together in the same file, as
User::Guest? Or even implement base class to have a generic abstraction (like
BaseUser) that will explicitly relate
The last one makes sense to me, but it doesn't feel like following Ruby way. It looks more like Java way :) Usually when I'm implementing null object, I just declare separate class. But I can't say I'm completely satisfied with that approach.
PS: I really like the way URL slugs are working on this forum.
/forum/proper-location-for-null-objects — neat!
This is a great question. In most cases, I'll put these into the models folder because they really operate like models. It's nice to have them side-by-side because often times if you change the User model, you'll likely also need to modify the GuestUser to be compliant. I usually put it in
app/models/guest_user.rb like you mentioned for simplicity at first.
You can always create folders inside your models directory as well so you could put it into
app/models/user/guest.rb as well and name it
User::Guest to keep it organized. This might be useful if you also include some User specific modules, they can fit in that directory as well.
Of course, you could even create an
app/null_objects directory if you had a lot of these for various purposes and wanted to organize them all into a single place.
Since Ruby doesn't do multiple inheritance, I guess a BaseUser might end up being more of a module to include than a class to inherit from since the User model will have to inherit from ActiveRecord but the Null object will not. You probably wouldn't get a big benefit from this other than maybe enforcing some method existence.
PS: The urls are generated by friendly_id 🤘
I'm chiming in with some practical experience, having written several medium-size applications with 50+ models.
http://blog.makandra.com/2014/12/organizing-large-rails-projects-with-namespaces (SFW) is one of the articles I read when I decided to start namespacing my models directory. It does a good job explaining "why" you would want to namespace your models folder.
In brief, it helps organize your files for another developer to easily understand what the main pieces are in your application, and it's just more manageable. Even though I use keyboard shortcuts to find/open files 99% of the time, it just "looks" easier on the eyes when the folders only have a handful of files in it.
Think of it like Ruby classes: there's nothing to stop you from throwing 500+ lines of code into a single ruby class, but almost everyone would agree that's a bad idea; it's recommended to break that code up into several classes that work together. Having a handful of main folders that organize the files themselves makes it easier to understand IMO.
In your specific case with a
User model and a
GuestUser model, here's how I implement stuff like that:
app/models/user.rb => the main "user" model
app/models/users/as_guest.rb => the "guest" user
class User < ApplicationRecord ... end module Users class AsGuest < ::User ... end end
Another practical example would be inviting new users. Typically, when you invite a new user, you want to validate that they enter an email/password combo properly and send them a confirmation email after they are invited. Instead of throwing this all into the
User model with conditional statements, I put that validation and email handling into the
Users::AsInvitation model. This allows me to create a
User object without having to send confirmation emails if I don't want to (for instance, for an API or admin panel).