Closed TheJoeSmo closed 2 years ago
It's not clear what problem this solves. Can you provide a use case where you would use something like this?
An observable is an event that can be subscribed to. This solves the issue of unneeded coupling of attributes that are being shared between objects. An observable promotes the idea of abstracting the attribute into a sharable attribute and connecting the objects to the attribute. By providing the abstraction the observable is tasked with handling the abstraction, reducing the burden on developing objects.
A great case to use observables is settings. Without an observable to subscribe to, the objects would be responsible for propagating the message across the entire editor. With a setting, the objects that needs a setting subscribes to the object and only receive updates when needed. Any object can equally set the setting and the observable would be in charge of notifying all applicable objects. The only cost of utilizing an observable is having the ability for anything to call anything as there is no coupling, which is why it is critical to promote naming of observables as you would a class, as an observable could be anything.
I also don't quite get what would need to be observed. Do you have an actual feature or problem, that could only be solved by this? In general, since python does not have private methods and many other obstacles like Java and C# many design patterns are simply unnecessary and complicate things.
Even at work, where we handle distributed robotic and factory management, we rarely actually go "I think we need a factory method with this facade", we simply try to add a feature, and if we encounter friction we try to change/add as little code as possible to make it work with a degree of cleanliness. If I end up adding a class, that could be called an Adapter, cool. But I certainly don't go through my list and see which pattern would work best.
We never write code, that we might need in the future, because 8 times out of 10 that day never comes and it just sits around, potentially causing problems and costing everyone else time, when they have to understand it.
This makes it hard to abstract out classes for GUI and do basic testing for those classes.
Why would we want to do that? Qt is a stable and actively developed GUI framework. So there's no real need to abstract us away from it.
Most of the widgets have a level_ref and connect onto its data_changed
signal, to update()
themselves accrodingly. So testing those should be as easy as creating a level, changing something and observing if the state of the widget changed.
I disagree, design patterns are just as useful in python as they are in java or any other statically typed language. The main advantage python provides is it automatically implements most of the 'creational' section of the original design patterns. Structural and behavioral design patterns, however, are just as needed as a given statically typed language. Just because you can use mix-ins for everything does not make it the best solution. Personally, I find using the bridge pattern in combination with dependency injection is much more efficient than a mix-in, seen with Observable
, providing more flexibility and testability than a mix-in. The degree to which you may use patterns is greatly determined by the paradigm utilized. If your code is more data oriented, then you will utilize a different set of tools to solve problems and so on. And on a fundamental level, understanding when you use a design pattern lets you explain what you are actually doing.
I did not design Observable
because it might be needed. I designed it with a specific purpose in mind. I intend on making a pr after this is accepted to rework settings to allow for a simplified system that allows multiple namespaces. I also intend on dividing GUI into two parts: the business logic and the GUI itself. I intend on adding a Plugin system that cannot work without the Setting system I created and thus we something to act like an Observable
or the whole system falls apart. I divided it into multiple prs so we can review the Observable
first and agree on a system that works before I go and implement the Settings system and add my GUI. So I can either close this pr and combine it with something that 'requires' it or we can agree on a solution in this pr.
I do not intend on testing if Qt works. What I am wanting to test is the business logic that we have wrote. Our business logic is only as tested as we make it and thus, we need something that allows for fast tests. You could argue that we should use the slots utilized by Qt, but I believe firmly that we need more flexibility and do not want to rely all our business logic on Qt. Qt has a lot of additional stuff under the hood that we do not necessary need and we'd still need to make an adapter to solve. This pr solves this by creating its own system of very flexible code that can be changed to the need of whatever is being wrote. You can even use Qt if you want with my system.
Finally, I will explain why we need these design patterns with a case study of LevelObject
. LevelObject
has a single method that is over 400+ lines of code. This code is primarily a bunch of ifs and elifs. No one can read this code easily and no one can make a test for this code in a reasonable amount of time. I know this because I read that code and it took forever to rework on my fork. Instead of writing this 400+ lines of code, we could of simply used the bridge pattern and separated each generator into an object that handles its specific arrangement of blocks. Inside the LevelObject
code we then could just read a line or two that calls the respective generator dynamically. This allows this 400+ lines of code to be split and thus tested in isolation, which increases readability and testability. Now someone can make a new generator without having to worry they're going to screw up some 400+ lines of code because they want to use a custom generator and better yet, we can prove their code works.
Business logic, dependency injection, bridge patterns. This is a level editor for a 20 year old NES game, we're not designing a banking application or self driving cars. I understand, that it is alluring to plan things out and use things like Patterns to feel like you're building a reusable structure, but 9 times out of 10, when you actually implement and need to work with it, you planned yourself into a corner and have to break your abstraction and adapter layers anyway.
And if you think a 400 line class with a bunch of if's is not readable, how will all these observables, bridges, what have you help with that? This thing is a tool for hobbyists, so, if anything, hobbyist programmers will try and make additions to it. So breaking out the software engineer enterprise patterns will only alienate the few people that might be interested in contributing.
Not to make an argument from authority, but I am a professional dev with a 5 year degree and I never (intentionally) use any of those patterns, because working in an existing code base means, making as few changes as possible and sweeping architectural changes never have a good return on investment.
I don't even have any mixins in the code. I actually like them, but again, they were not necessary and the code is still clean enough to be understandable and extendable by novice programmers. I don't want to have to look up how some observable works to add a new setting, if it is not absolutely necessary.
I'd rather the thing, that actually needs this code 3 PRs down the line to work with how things are right now or at least to get through all the changes that are compatible, before we look at fundamentally changing significant portions of the code base. And if we then find out, that the current code is just too crappy to get us there, feature by feature, we can see what needs to be updated to get it working.
You are missing the entire point of the patterns. Dependency injection is not complicated, you just make your dependencies arguments. The argument for why its better than an alternative might be, but to code using dependency injection is simple by nature. Same goes for the bridge and other patterns as well. The great thing about patterns is that you run into them even if you refuse to learn the patterns yourself. The patterns do not make things any more complicated than they already are, if anything they make it easier to understand. Why worry about 400 lines of code when I can just create 20 lines and change a yaml file. How does writing less code result in alienation?
I didn't even use many patterns in this commit. The only general idea I used was dependency injection, which doesn't change much other than how I write a few unit tests. There is nothing fundamentally complicated going on and very little actual code is added. I know I need to add this as I've already added the settings and GUI on my fork, so I don't need to ask will I need to make this because I know I need to make this in order for it to work. So what I am implying is that we do not have an alternative and we must add something new and I cannot make my features without this addition. If you want me to make another pr that requires this so you know that indeed this thing is required then I can make another pr for my settings or GUI and it'll use this specific thing. The only point of this pr is to make it make logical sense in terms of progression and allow us to finalize the design of the Observable. If you want me to make a pr with the settings that require this pr before you accept it I can do that, but that fundamentally defeats the purpose of this pr.
Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Currently, there is no good easy way to make an observable event that is easy to debug and work with. This makes it hard to abstract out classes for GUI and do basic testing for those classes.
Describe the solution you'd like A clear and concise description of what you want to happen. Create a custom observable class to create observable events from.
Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered. We could extend the slots and events in pyside. I decided against this as it is would force us to use their implementation of an observable events and would require roughly the same amount of code to achieve. Also, pyside may force additional initializing requirements for testing, which would result in tests being greatly slowed down compared to the alternative.