I’m creating a video player, like a custom YouTube player.
All of the GUI elements (progress bar, video player, play button, …) are different classes, but I obviously need them to communicate. When the progress bar is clicked, or the slider is moved, it needs to send a “seek(x)” command to the video player. Similarly, the video player needs to update the progressbar every frame.
Currently I’m doing this by having almost all elements have a link to each other.
So when I create the progress bar, I’m telling it where the video player is.
But after a while this becomes more and more complicated, and I’m wondering if events would be a better way to do this. Or a main controller class that has all the connections.
What should I do?
6
You should combine events, states and methods.
Events should only inform that the event happened, not more.
States of the application should make you decide what can and cannot be done at that moment.
Methods should do the job like starting/stopping playing the video etc.
Events should not be used instead of methods, because it becomes harder to read and understand the code. Just change states and call some methods from your event.
0
Ideally, your GUI classes should have no idea they are even part of a video player. They interact with the user and that’s it. This is called the Single Responsibility Principle.
In your case, I would create a TimeBase
class to keep track of what frame is currently playing. It would have seek()
and nextFrame()
methods to change the current frame. Objects would call addSubscriber(subscriber, interval)
to receive events when the current frame changes. The interval parameter is for widgets that don’t need a one-frame resolution, like a display that changes every second, for example.
The next layer down would be adapter classes for every widget type, which implement both the TimeBaseListener
interface and the click/slide/whatever listener interfaces from the GUI class, and translate between them. For example, a ProgressBarAdapter
would get pointers to both the TimeBase
and ProgressBar
objects during construction. It would listen for slider moved events, translate the new slider value into a frame number, and call timeBase.seek()
. When it received a FrameChanged
event, it would translate the frame number into the progress bar value and change the GUI accordingly.
A lot of people balk at the second layer, preferring to roll that functionality into a class derived from the widget class. A lot of tutorials on languages’ official websites even use that method, but they shouldn’t. It violates both the single responsibility principle and preferring composition over inheritance.
In less buzz-wordy terms, the problem with inheriting from the widget class is now you are locked into its inheritance hierarchy, and can never choose one more suitable to your application. You also can’t split the class when it gets too big. What works okay on small website tutorials illustrating one small concept will not scale well into a real life application.
My rules of thumb for using events:
- can you imagine these components existing separately, used not only in conjunction with each other? If yes, most likely go with events.
- do you need from this component some information in response to the event sent? If you don’t, then, once again, most likely, you’ve made right decision about sending events.
This does not free you from good and well-thought-out OOP-architecture, since even event handlers shouldn’t be spagetti code.
This does not free you from difficulties of debugging such code, which by its nature tends to be asynchronous.
But, nevertheless, you can sum it up as follows:
If you think of your system like of a set of autonomous components that should be easily interchangeable/used with any other (sub)set of components, use events.
Every GUI framework uses some kind of event publishing/handling, so you are forced to use them.
When designing a GUI application, you should use MVP design pattern (or one of it’s variation, like Presenter first, or model-view). This way, you can easily unit test the code and logic. When the code is unit tested, it is usually simpler to understand (off course, there are exceptions).
Also, do not put everything in one MVP triad, but try to break your application as much as possible. Smaller chunks of code are easier to understand.
Looks like what you’ve got is the best bet for now. You want to keep your links to a minimum, but there needs to be a connection between the progress bar and the video player, so at least one needs a reference to the other. Having all the connections in one controller means more work when one element wants to talk to another.
Events won’t save you. If a dozen (or even two) different elements, unknown to the player, needed regular updates on progress, an event would be just the thing. Note that the player would now be setting off the event rather than calling a progress bar method, so you’re pretty much breaking even in complexity. (But now it’s harder to figure out who’s being informed.) Here, I think a simple method call is better.
The elegant solution–the one probably floating around in the back of your head that led you to ask this question–would be to reverse the responsibilty and have the progress bar ask the player what the progress is. This takes a load off the player–it doesn’t have to worry about when to make the calls–though it does have to have progress information always ready. There are no fancy events, just a simple method that someone else calls. And the player doesn’t care how many other elements call the method, or when, or why.
The down side is the progress bar needs to run a timer to keep things going. But the player would have needed to do something similar: the work and the responsibility are now concentrated where they belong.
I’m not sure you need this “elegant” solution quite yet, but I think that’s the direction you’ll want to move in later.
(If you use it, be sure to run everything on the EventQueue, or UI queue, or whatever your language calls it–at any rate, keep everything on one thread. Multithreading is very tricky, and in most cases UI’s only work on their own thread anyway.)