I was working on a new feature at work and discovered a nasty conditional that determines where to redirect the user after they log in.
For some reason, this is the time it hit me: every single rails app I’ve ever worked on that had any amount of real-world usage had this conditional. Every single one. It got me thinking about why.
Reminder: Inherent vs. incidental complexity
Rich Hickey has a great quip about this. Inherent means “your fault” (the business), incidental means “my fault” (the programmer). It really means inherent complexity is a fact of life - the system we’re modeling has inherent complicated behavior or expectations. Incidental complexity is what we as programmers add to the inherent complexity in order to get our jobs done (or for other reasons!)
Now, here are, I think, the reasons that redirect logic is often complex:
Multiple user roles. Different types of users often see different things after they log in. Inherent complexity.
Conditional redirects. If this is the first time the user logs in, or their account is in an incomplete or invalid state, we need to take them to different places. Inherent complexity.
Permissions. Different users, even within the same role, have different permissions to see different things or take different default actions. Inherent complexity.
Third party auth. Integrations using third party authentication libraries or services often have idiosyncrasies we need to manage around. Incidental complexity.
All of this can mean that your redirect logic is hard to test, hard to document, and/or get your head around. Which really means hard and risky to change.
Here are some strategies for simplifying complex redirect logic:
Use a single method. Instead of keeping the redirect logic in your post-auth controller action, put it into a method. Devise encourages this in its design, but it need not rely on your auth library’s conventions.
POROs or service objects. If your conditional is particularly hairy, consider extracting it all into a PORO or a service object that can be unit tested. Call `redirect_to RedirectService.new(user: user).call` and be done with it. You could even unit test that object.
Factories. This is my favorite. Collapse the code path into a single code path by only switching on the class. Then call “redirect_url” on the result. This is the approach that is easily unit tested, especially for complex logic, and those tests can elegantly act as your documentation. And, it lets you expand the number of objects to handle each case, and potentially include idiosyncratic logic in each case. You may have an `Admin::BillingRole` object that takes the user to the billing dashboard, a `Inspector::ServiceRep` object that takes the user to the Tickets#index screen, and so forth. Each object can be small but they all play the same role.
Have you encountered this? What strategies do you use?
I have always like to model an app's navigation with a DAG. If a child node needs to redirect it can send a message up to its parent and either take action or continue to pass it up to the next parent.
Dave- Thanks for sharing this. Admittedly, redirect logic is outside of my usual stomping ground. So this is a refreshing find. Hope you're well this week. Cheers, -Thalia