Video transcript & code
There's this classic Isaac Asimov story. It's about a particularly clever robot who lives on a solar power generation station in space. The robot's job is to keep the power relay beam focused tightly on the receiver station on Earth. The robot has lived its whole life on the station, along with two humans and a bunch of other robots.
One day the robot has a bit of an existential crisis. It questions whether humans actually created it, and whether Earth actually exists. After all, all it has as evidence are sensor readings, which could easily be simulated.
Put yourself in the place of a robot like this one. Only instead of being trapped on a space station, you're stuck inside a typical web application.
There are signs everywhere of a mythical creature called a "user". There's a
current_user method that gets called a lot.
class NotificationsController < ApplicationController before_filter :ensure_logged_in def index user = current_user if params[:recent].present? notifications = Notification.recent_report(current_user, 10) if notifications.present? # ordering can be off due to PMs max_id = notifications.map(&:id).max current_user.saw_notification_id(max_id) unless params.has_key?(:silent) end current_user.reload current_user.publish_notifications_state render_serialized(notifications, NotificationSerializer, root: :notifications) else offset = params[:offset].to_i user = User.find_by_username(params[:username].to_s) if params[:username] guardian.ensure_can_see_notifications!(user) notifications = Notification.where(user_id: user.id) .visible .includes(:topic) .order(created_at: :desc) total_rows = notifications.dup.count notifications = notifications.offset(offset).limit(60) render_json_dump(notifications: serialize_data(notifications, NotificationSerializer), total_rows_notifications: total_rows, load_more_notifications: notifications_path(username: user.username, offset: offset + 60)) end end def mark_read Notification.where(user_id: current_user.id).includes(:topic).where(read: false).update_all(read: true) current_user.saw_notification_id(Notification.recent_report(current_user, 1).max) current_user.reload current_user.publish_notifications_state render json: success_json end end
There's an enormous
User class, with a dozen different responsibilities.
class User < ActiveRecord::Base # ...1000 lines elided... end
User class doesn't seem to represent a living entity. When the application services requests, they don't come from this
user object. When we send out responses, they aren't sent to the
def index render "index" end
In what form do we actually interact with this supposed "user"? Well, we receive requests that include parameters that theoretically represent a user's intentions or desires.
def create if params[:expedite_shipping] # ... end end
And we send off HTTP responses to… somewhere.
Again, none of these actions seem to directly begin or end with the object called
current_user, which is confusing.
In fact, when we strip away all the functionality that is only used in a specific context, the
User class seems to have just one consistent purpose: mapping a set of credentials—say, a login and password—to a set of roles or permissions.
Reduced down to these fundamentals,
User seems like an increasingly strange choice of terms for this object. According to the dictionary, a user is supposed to be a human person who uses a computer program. But even assuming these "humans" actually exist, a
User object isn't always associated with any such beast. Some "users" are purely "system users", only used by automated processes. Other "users" are assigned to whole organizations, and shared among members of those organizations. On the flip side, there are some humans who possess and switch between multiple "user" logins.
In fact, if we were given this description as a specification, we would never have named these objects anything so misleading as "users". We would have called them "accounts", because at their core that's what they are.
class Account < ActiveRecord::Base end
And as for the actual users? We don't really have an object in the system to represent them. And truth be told, we never interact directly with them anyway. What we interact with are HTTP clients, some of which are browsers acting on behalf of the users. Or so we are told, anyway.
This, then is the situation in most modern web applications. We have clients, which we never actually refer to directly. We have user objects, which represent nothing of the sort. And these user objects are then larded down with what seems like half the code in the application, because after all, everything the app does is for users, right?
What would happen if we called "accounts" exactly what they are? This might immediately reduce our temptation to put unrelated functionality into these objects. After all, an "account" doesn't buy a book or send a friend request.
But then do we need some other object to represent, you know, actual people? No, we don't. Because those objects already exist. They are called actual people.
A persistent misconception about object-oriented programming is that we're supposed to be modeling objects from the "real world". But this isn't what OO is about at all. As Rebecca Wirfs-Brock writes in her book Object Design:
We represent real-world information, processes, interactions, relationships… by inventing objects that don't exist in the real world. […] We take difficult-to-comprehend real-world objects and split them into simpler, more manageable software ones. […] Our measure of success lies in how clearly we invent a software reality that satisfies our application's requirements–and not in how closely it resembles the real world.
Once we stop pretending that we are modeling users in software, we can instead start creating objects to represent the tiny, imaginary facets of people that are actually relevant to our domain. Usually these facets are based on relationships that human beings have with the program or with the organization that owns the program. For instance:
Any one of these objects might be associated with zero, one, or many accounts. And a single account might be associated with more than one of these objects—for instance, one person's account might be associated with both
TeamLeader objects. But the account itself isn't an employee or a team leader. It's just an account.
Naming matters in programming. The names we give to objects influence how we divvy up responsibilities as we become aware of them. When it comes to naming choices that have influenced program design on a massive scale, it's hard to think of a better example than the choice to name the central abstraction in our applications "user".
It's a choice that's misleading on two levels. First, because it inaccurately identifies the concept actually being described. And second, because by existing, it implicitly suggests that it is even possible or desirable to model such a concept in code.
The result is a situation that most web application programmers are unfortunately familiar with, where the
User model seems to grow and grow without limit. And where sometimes we act like it really is the user, and other times we treat it like just a dumb account.
I'm going to be phasing out the use of the term "user" in my own projects. I'm going to call accounts what they are, and I'm going to let the users—or as I like to call them, "people"—stay outside the program where they belong. If you decide to join me in this experiment and have feedback or questions, I'd love to hear from you.