Doesn't that already look 100x better?
class UserCreator
def create_user(email)
user = create_user_object(email)
assign_admin_role(user)
assign_invite_permission(user)
call_invite_template(user)
end
private
def create_user_object(email)
User.create(email)
end
def assign_admin_role(user)
user.roles << find_admin_role
end
def assign_invite_permission(user)
user.permissions << find_invite_permission
end
def call_invite_template(user)
find_invite_template.call(user: user)
end
def find_admin_role
Roles.find_by(name: 'admin')
end
def find_invite_permission
Permissions.find_by(name: 'invite')
end
def find_invite_template
Templates.find_by(name: 'invite')
end
end def create_user(email)
user = User.create(email: email)
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
end
IMO this 4 line function is significantly better in terms of clarity, readability, and it avoids unnecessary state. Testability and encapsulation are the same.(I would argue that the encapsulation is better with a function, since encapsulation is way too easy to break in Ruby, but hey, that's me)
E.g.
lambda do
state = 0
->() { state += 1 }
end.call
(Note the `.call` at the end - we don't want the outer lambda; that is just used for creating the variables that will hold the state - we want the inner lambda)So let's say that for some stupid reason your user creator object needs to keep a count of users, you could "seal" that state into a lambda with the above technique, and make it really hard (I'm not sure if impossible) to break encapsulation. E.g. instance_eval or trying obtain the binding doesn't work, because they both will get you the state of the Proc object containing the closure, not the local variables accessible within it.
It provides some very marginal improvement, but doesn't address the actual problem of the code, which is the horrific verbosity. The use of attributes is the least of my problem with it. This is how I'd want it to look:
class UserCreator
# Arguably, I'd prefer *call* because that allows it to be interchangeable with a lambda
# But frankly this thing could *be* a lambda. E.g. you could replace the above class declaration
# with "UserCreator = ->(email) do" and ditch the "def"
# The exception, where I'd allow for an initialize and attributes would be in cases where you'd otherwise
# be passing a *lot* of *the same* state around between multiple methods
#
def self.create_user(email)
User.create(email).tap do |user|
user.roles << Roles.find_by(name: 'admin')
user.permissions << Permissions.find_by(name: 'invite')
Templates.find_by(name: 'invite').call(user: user)
end
end
end
The whole service object pattern is heavily abused by people who don't seem to understand which (limited) situations it actually provides benefits. This isn't one of them.I kinda like the Clean Code advice of small functions/methods, but they should pack a punch, not just encapsulate barely more than their own name.
E.g. here's a single-line method from the Window class of my Ruby X11 window manager:
def type = (@type ||= get_property(:_NET_WM_WINDOW_TYPE, :atom)&.value.to_i)
This does one thing: It queries the X server for the _NET_WM_WINDOW_TYPE atom attached to this window, and if a value is returned, it ensures the result is an integer (which will be 0 if what is returned is nil or somehow can't be parsed into an int, which would mean someone has been very naughty but defaulting to 0 is fine). It then ensures the value is memoised in `@type` (not doing more error checking here is fine, because failing is worse than defaulting to not knowing the window type, as we have no guarantee it's set in the first place)Tiny functions like that are fine, because they encapsulate a sufficient amount of non-obvious logic in a very concise result. Being able to write `my_window.type` without caring about that logic is great. Trying to reduce your methods to compact little methods is great when it makes you write little functions that tells you a lot.
Unfortunately - and so I agree with your reaction to Clean Code - adhering to the rest of the Clean Code advice won't leave you with a bunch of little methods that pack a punch and remove a bunch of complexity, but a bunch of little methods that each encapsulate barely more than their method names worth of complexity.