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).
1
First, I would like to point out, that your observations are “valid”. When I made similar observations back in the day (it was Java Enterprise applications for me) people wanted to talk me out it, like I’m seeing it wrong. Just use this-or-that pattern, layered architecture, variations on MVC, etc.
Each time we tried to “decouple” UI and “business” (at least in the “traditional” ways), the end result was that things that really changed together all the time were separated among different layers, even different modules at times. This is not readable nor maintainable. Customer would come to us to “just add a field” somewhere, we would go out and try to change multiple layers and modules to weave “data” through multiple views and services, etc., with multiple days of work.
Seems cleaner.
I blame Robert Martin. 🙂 We developers have all different shades of OCD. 🙂 Symmetry and “cleanliness” is just too subjective, objective things like practicability and readability should overrule it every time.
So what helps? First, try to co-locate things that belong together. This helps enormously with readability and maintainability since you don’t have to dig around to find out how/why things change.
When are things co-located? The easy rule would be to not let “data” out. Don’t “get” data from others. That’s it.
Second, don’t do technical separations. Quite the opposite, make sure all things are business related and align with the domain. Try to hide technical things as much as possible below domain-relevant signatures. The more your code is aligned with business concepts, the easier it will change when those business concepts inevitably change.
Note here, that I consider the UI “business”-related, since all people involved will talk about the UI and will actually often talk about new features in terms of the UI.
I have no clue how that can be applied to JS and Angular, but hopefully you can translate that somehow. 🙂
It sounds like you need some events in your new business logic.
Before any UI part calls joinChat it can subscribe to the relevant events. During the execution of joinChat, which would entail multiple steps/stages, the event handlers would be called. This would allow the UI clients to provide feedback to the user, to provide additional information to joinChat or to cancel the entire procedure if desired. And the BL would not need to know anything about any UI.
Do not forget to unsubscribe (preferably in a finally clause).