Which of my widgets should be responsible for creating an object with a long lifetime
-
I have the following design:
A main window which has a widget, and inside that widget different widgets of type "Page" can be loaded. The first page, let's call it the startup page, allows the user to type in the serial number of the product. The database is then queried to get some info about the product itself, and then a concrete child class representing the type of "Product" is created. All child product classes inherit from an abstract class, Product.Basically, when the user selects the product by entering a serial number, a product factory is called to create the concrete object and then check if it's physically connected to the system via a bus. This created product will then be passed to and used by other pages in somewhat of a wizard like fashion.
What I can't decide is where the logic should live for both creating this Product object and managing the lifetime. One option is my startup page could be quite dumb. User enters serial number, presses a button, and emits a signal with all the info needed to construct the Product object. My main window can connect to this signal and take on the responsibility of creating the Product object and managing its lifetime at the highest level of the application. This is nice because the object is created at a level which its lifetime will be managed.
The second option I see is to have the startup page create the concrete Product object when the serial number is entered. It could then check that the product is connected to the system, and emit a "ProductConnected(Product* product)" signal. This is nice because the logic is all encapsulated inside the page whose job is to load a particular product and then main page can just be given an already created product. But now there seems to be a decent amount of ambiguity as far as the object lifetime and where it's managed. I also don't know if passing a pointer to a QObject through a signal is kosher. I can definitely see some potential issues.
Any guidance about pitfalls and suggestions with regards to these approaches (or one I haven't though of) would be appreciated.
-
Hi, welcome to the forum.
The biggest problem I see with both of these approaches is that the ui is kinda driving your logic. Typically that's a bad idea and down the line leads to problems that are hard to foresee in the planning stage.
The way I would approach it is that your app is a state machine. There's user input stage, product lookup stage (which may fail from what you describe), product view/editing stage etc. Transitions between those stages would dictate a ui change, for example switching from product id input to product lookup would switch ui page from input form to a wait indicator and finishing the query would move either to success or fail state, which would show an error page or the product page accordingly. The product object would then be governed by the state machine state that handles it. Lookup would produce an object and pass it to the view/edit state and that state would free it when transitioning to another state. Your logic would be better encapsulated this way and independent of the ui, which you may want to change someday or make optional e.g. drive the state machine in a silent mode, app params or something else. The ui would just send a signal that would change the machine state e.g. entering a number would transition the machine from input to lookup, which in turn could be connected to page change.
See Qt State Machine C++ Guide for a description of Qt's state machines support and examples.
-
@Chris-Kawa said in Which of my widgets should be responsible for creating an object with a long lifetime:
stage, product lookup stage (which may fail from what you describe), product view/editing stage etc. Transitions between those stages would dictate a ui change, for example switching from product id input to product lookup would switch ui page from input form to a wait indicator and finishing the query would move either to success or fail state, which would show an error page or the product page accordingly. The product object would then be governed by the state machine state that handles it. Lookup would produce an object and pass it to the view/edit state and that state would free it when transitioning to another state. Your logic would be better encapsulated this way and independent of the ui, which you may want to change someday or make optional e.g. drive the state machine in a silent mode, app params or something else. The ui would just send a signal that would change the machine state e.g. entering a number would transition the machine from input to lookup, which in turn could be connected to page change.
Thanks, this is helpful. So, as far as I am understanding it my MainWIndow widget would connect to state transition signals it wants to observe, and as the particular states were transitioned to/from it would update the UI accordingly? So, for instance, it would connect to the entered signal of the Login, Login Succeeded, and Login Failed states. When the connected slots are called they would handle updating the UI state, notably, removing the old page and loading the new.
Two follow up questions:
-
Let's say my MainWindow creates the login page dynamically when the state is transitioned to, not immediately when the application starts. So, the state machine transitions to the "Login" state, the MainWindow responds to the Entered signal of that state and creates and inserts the Login Page in its connected slot. Now that the Login Page is created, doesn't it need to connect to the different state transitions? The only way I can think to do this is to have my main window keep pointers to all the individual states so if the page is created later on at runtime it can connect up as required
-
If we are in the state where the lookup product page is being shown and a user clicks the "Lookup" button, where does this logic for the lookup and product creation exist? My assumption is it would work something like this. The Lookup Button is pressed, the slot is called, and a database query happens. If the product is found, a "productFound(new Product())" signal is emitted. If the product is not found, a "productNotFound()" signal is emitted. The state transitions will be connected to these signals and change accordingly. I assume this decouples things because if you removed the UI, you could connect whatever you want to these signals, it doesn't have to be the UI.
Am I on the right track?
-
-
There's no single right way to do it. The point of what I suggested is not to make it harder for the sake of it. It's to avoid common pitfalls that occur when designing app ui first. That being said you have to decide what's best for you.
But to give you some ideas to work with:
- One way would be to have a single state machine that has all the transition signals exposed, a ui controller that has slots for all of these and signals for all the ui actions and just connect those. When you create a new piece of ui dynamically you manage the button connections on the ui side only e.g. between a button and the ui controller.
Another way would be to have an interface with all the transitions defined and the ui class would just implement it. All the connections to the interface would then be managed on the state machine side and again - adding a ui element would be a detail of the interface implementation.
But there's always downsides to every design. If you have a lot of states the above can become a burden - a large interface or ui manager. If you want to avoid that you can think about it differently and have each state be responsible for creating their own input/output interface. On transition in/out of state it would call some factory to provide a class that implements its ux, either through a ui, automated or other.
In any case I would refrain from making MainWindow create any pages. You can make MainWindow double act as ui manager, but that is again sorta mixing responsibilities. MainWindow is just a piece of window you can put ui stuff in. It shouldn't really do much more than that. - Again - no single solution, but the ui should not do any logic. It's just a visual representation of state and user input vehicle. In a state machine all the logic is done in the state nodes. In the example you gave you're again sorta moving the logic towards the ui. The way you could do this instead is something like this: Have a user input state, lookup state, product state and failed lookup state (which could be the same as user input if you go back in that case). Each of those states has a separate ui page created/destroyed when the state is entered/destroyed. This could be done either by signals/slots to a ui manager or in the state itself via some factory like I mentioned above.
By the way, it's worth mentioning that Qt has a QWizard class. It implements something like that. The class is a state machine that manages the states, transitions and gives you entry points for each state. You can create/modify/destroy the ui of each page on transitions. It's not suitable for all cases, but a a linear logic like input->lookup->display may fit this use case well, so see if that's something that could be of use to you.
- One way would be to have a single state machine that has all the transition signals exposed, a ui controller that has slots for all of these and signals for all the ui actions and just connect those. When you create a new piece of ui dynamically you manage the button connections on the ui side only e.g. between a button and the ui controller.
-
@Chris-Kawa Awesome, thanks for the feedback
-
@Chris-Kawa Just wanted to come full circle on this. What I've ended up doing is I have a StationController class which is injected with a StateCommander class. The Station controller then builds the state machine internally and connects to the concrete StateCommander's signals to be used for transitions. The state commander can be a UiCommander or a CLICommander etc. The base StateCommander class has methods such as LoadProduct which the UiCommander can override if need be, but the base class will just emit a RequestProductLoad signal.
When the Commander is passed to the StationController, the StationController registers for the Commanders signals. So the UiCommander could contain a reference to the Ui components it needs, but a different commander wouldn't need to know anything about the UI. In this case I have a UiCommander which forwards button clicks signals to the generic signals the StationController is connected to. The state functionality remains static and is not coupled to a UI at all, but instead is coupled to just an abstract StateCommander. I had come across this example and really didn't like it because the states were so tightly coupled to the UI. The states take a Scene as an input and I knew I didn't want my states taking in UI components. By delegating the triggers to my StateCommander class, the state machine has no knowledge of whether the system is headless or not and the states can transition along just fine.
I then have a UiController which contains the main window and it registers for events from my StationController and can update the MainWindow accordingly by calling main_window->setPage(page); The main window is just dumb and displays whatever the controller tells it to.
I really like the decoupling but one thing I'm not a huge fan of is that the state is driven by a single Commander object, and if the station gets more and more complex, I will have to keep cramming functionality into that single interface. But, I think that's what you eluded to in the post I'm replying to, and I can live with that for now.