I am refactoring a legacy codebase of an Angular SPA. The central entity of the app is the chat room, and there is a plethora of ways on how to enter a chat from different views all across the app. Due to our special use case, entering a chat is a multi step process with a fair amount of possible edge cases. To top it all, it has to work on mobile browsers, too.
Before my refactoring, large chunks of the chat joining precedures were simply implemented within the respective buttons' click handlers. Lots of duplicated code, business logic in UI components, I don't think I need to explain why it was hard to maintain.
Now, what I set out to do was to implement a central joinChat
method in a service. One that -of course- is separated into smaller subroutines, but which you can read from top to bottom and understand the flow. However, while doing that, I noticed more and more details on how each button triggers a slightly different chat joining procedure, regarding which confirmation dialogs to show, which pre-checks to do, etc. Especially the mobile version works quite differently.
Now, what I ended up with is a joinChat
method with too many configuration parameters. Sure, it's way less duplicated code now and you don't have to search all over the app anymore to change a detail in the flow, but I feel like I just transformed one anti-pattern into another. Even worse, instead of having business logic inside the UI, I now have UI inside the business logic, as subroutines of joinChat
trigger dialogs and alerts. If the designer comes along and demands that a certain confirmation dialog should e.g. be a modal on the desktop version, but a separate page on the mobile version, things will get messy.
How do I approach this elegantly?
My current idea would be to let the business layer contain only atomic steps of the chat joining process and move the orchestration of these steps somewhere else. Where exactly? I see two options:
- Back into the UI components. It seems like a sin, but also like a pragmatic thing to do, given that the flow is so interdependent with the UI components anyway. The use case "show a modal on desktop, but a separate page on mobile" would be trivial to implement. On the downside, a flow that encompasses multiple screens would result in hard to read code, as someone who wants to understand it would have to reverse-engineer it screen by screen (one of the biggest issues I faced with the old codebase).
- Into a controller layer between the UI and the business layer. Seems cleaner. Use cases can be read from top to bottom, even if they traverse multiple screens. Dependencies on user interaction would be solved using a delegate pattern. However, most other parts of the app don't require such a layer as they are pretty straightforward CRUD, and it feels inconsistent to now introduce it only for this one use case. Another problem I see is that it would contain many similar entry points, e.g.
joinChatFromContactsList
,joinChatFromTicket
,joinChatFromCall
,joinChatFromCallMobile
etc...
The main aspects that I want to optimize for are readability (because we are a growing company and new employees should understand the code easily) and flexibility (because we have to be able to react to changing requirements quickly).
join chat
functionality and see which variations are intentional from their side (for example when joining from the contacts list versus joining from a ticket) and which are not intentional and should be aligned (like mobile vs desktop). Hopefully, that already reduces the complexity.