A discussion about composition and code complexity

“Signal up, function down” Nice, I like that!

Of course. That’s why I made the thread. Don’t get me wrong. You’ve not wasted your time here. I just mean that I’m at a stage where I’m playing around with things. Getting a feel for what works and what doesn’t. I do have some long term project goals, but I’ll put my serious cap on when I know more about Godot. For now, I’m a curious kid trapped in a stubborn old man body, and I want to get into some trouble.

Funny, I saw at least two posts this week, with code that contains those exact InputComponent and MovementComponent classes. Looks like they all have been drinking the kool aid served by the same arch-speculative generalizator dude.

The first lesson that should be taught to any budding object-orienteer is Occam’s Razor. It should be the main governing principle when designing object oriented software systems.

Sadly it’s rarely mentioned at all when fancy concepts and terminology like “SOLID”, “encapsulation” or “design patterns” are tossed around. The Razor is centuries old and battle-proven across many scientific fields. There’s a reason it survived that long - it works!

Never introduce entities if they are not necessary. Never use a more complex device or concept than needed. If a function can do it, don’t introduce a class. If a code block can do it, don’t introduce a function. If reducing code can do it, don’t add code.

It’s also applicable to names. When I see a word “Component” (or “Manager”, “Factory” etc…) in class names I know problems are lurking down the road. It’s just pure redundancy for no other reason than wanting one’s code to appear more “knowledgeable”.

Godot has, what, about 100 node classes. How many of those node classes you see having the word Node in their names? Only 3 at the very root of the class hierarchy. Would you enjoy the company of AnimatableBody3DNode, Camera3DNode, Sprite2DNode, HBoxContainerNode… Of course not. You’d scream “Enough with this node node node thing already, we know it’s a node!” Same for Resource classes. Only a single base class is called Resource. There’s no ImageTextureResource or NavigationPolygonResource

Strive to design systems that look and operate stupidly simple, not intelligently complex. Beginners doing OOP often look like kids pretending to be grown ups, aping the appearance of those strange complicated “grown up” things without really understanding them. It always looks uncanny and they cannot fool anyone. If you’re a kid - do kid stuff first and foremost. And many times the grown ups benefit from doing kid stuff as well.

In systems design, the objective metrics of complexity are the absolute number of encapsulated boxes and, more importantly, the number, length and entanglement of communication noodles stretched between them, also known as spaghetti.

Have you started using again lately? I mean LLMs for making posts.

I admit, @normalized has much wisdom as well. Everything he said was valid. He makes an especially good point about naming. Even when I do make components, I do not name them that.

Case in point, I made a CollisionSprite2D a while back, and yesterday I turned it into a Collision Sprite 2D Plugin. I tried to follow Godot naming conventions in that there is an AnimatedSprite2D. In the case of CollisionSprite2D, it is just a Sprite2D that creates it’s own CollisionPolygon2Ds based on its texture at runtime. So you can have accurate collision detection without having to do it by hand.

Cute.

My definition is cognitive complexity, as measured by various metrics including cyclomatic complexity, the naming issues you and I both harp on, tightly coupled code (creating that spaghetti), and both functions and classes that are too long. I’m a big fan of DevOps and when possible I run something like SonarQube against PRs. (If they supported GDScript, I’d be using it on my games.) It measures a number of things and helps developers make code that is clearer and less complex.

LOL You know I would never do that. But yeah, I think my first reply was probably my longest post on this forum.

I did recently see that someone else on this forum watched the same tutorial that I used as my launching board into composition. It had pretty good info at a high level, but like I said in my first post, I question many of choices he made.

I’m probably pretty bad about that with functions, then. I tend to break stuff into tiny functions because it helps me understand the code when the main looping body just has a small list of functions. So I take it “single responsibility” isn’t a hard rule to follow?

Point taken.

Hah! This made me laugh. I’m 39.. so of course I’m a kid pretending to do grown up stuff!

Sick burn. And by burn, I mean the planet from all the LLM data centers.

Well shucks.

Yeah, a lot of bogus stuff in that tutorial. You had a healthy intuitive impulse to question it. Follow that impulse. The problem with tutorials like this is that it all comes in a neat high production values package, confidently delivered, using cutesy assets… it’s easy for a beginner to fall for it as a source of truth and example of good practices. But looking closely at that specific video, the results he ends up with are way worse than just doing it in a single script. While proclaiming “decoupling”, the three classes in this trivial example got so tangled up that even he gets confused with what he’s doing. Quite funny.

“Single responsibility” is a phrase that means nothing. Everyone can project whatever they want into it. For you, and perhaps Uncle Bob as well, it means ridiculously small functions, for somebody else may mean logical grouping of functionality, etc…

Think about this for example: Godot’s RenderingServer class has thousands of lines of code, doing all kinds of stuff in functions of all sizes. Does this class follow the “single responsibility” principle? Your “as small as possible” interpretation of “single responsibility” would conclude that it doesn’t and in fact fails miserably at it, but I could just say it actually has a single responsibility - serving the rendering functionality. The meaning of “single responsibility” is more stretchable than fresh pizza dough, and thus cannot be taken as any kind of objective guideline or, god forbid, a rule.

Making your functions as small as possible will chop up your code and make it mentally hard to follow and debug. It may be fine when the project is relatively small and you might feel confident you have it all under control. But once you cross a certain size threshold, hordes of small functions will become very taxing to comb through.

The complexity of the architecture is not at all evident by looking at how neat and petite individual functions or classes are. It’s how they’re interconnected that matters. That’s what makes or breaks the architecture, and that’s where real spaghetti happens. Many small functions tend to form much more dense and chaotic connectivity network, with very high entropy factor.

I just looked at that video and there’s so many things wrong in the 5 minutes I viewed. Looked at his channel, and he’s got experience as a web and full stack developer. He knows how to program, but he doesn’t really know how to use Godot. His “Inheritance Trap” is because he’s clearly a web developer who doesn’t know how inheritance works.

He then goes on about “The Godot Way” and says Godot is all about composition, when it is in fact about inheritance AND composition. He then exports things to link things together. This is a beginner mistake that I see all the time in tutorials where people know how to program, but don’t know how to use Godot. @export variables for connecting nodes is often the wrong way to go. @onready variables are usually the solution. And in the case where you want something more reusable and modular, you can search for the components up and down the chain as needed.

Don’t follow this guy’s advice. He does not know how to use Godot - he just thinks he does.

Back when I was a wee lad, before YouTube was even a thing, I learned Softimage XSI, 3DS Max, and some basic programming from a website called 3DBuzz. The idea was revolutionary at the time: Free video training by an actual professor, mailed (as in real, physical meat space) to you on CD. And what I really loved about their teaching style is the main instructor had an aid that would ask ‘dum dum’ questions during the tutorials, acting as an advocate for the student at home. Not once have I ever again seen their caliber of online teaching, so yeah, I question ALL of the content on YouTube.

Noted. Thanks for looking out, guys. I did actually ask him in his comment section about some of these choices, but I never got a response.

Oh I missed your edit here. So what IS the deciding factor in making something a function or a class, then? Is there a rule to follow on that, or is it just something that comes by feel with more experience?

By the way, I renamed the thread title and removed your answer as solution, @dragonforge-dev Sorry! Not because you’re wrong, but because the thread offers more nuance that may be helpful to others in the future, so perhaps they will read it through rather than jump to one conclusion. :slight_smile:

Repetition. It’s as simple as that. If you see something that repeats - it’s a candidate for extraction. If a passage of code repeats - it’s a candidate for a function. If a broader pattern of multiple pieces of code and data it operates on repeats - it’s a candidate for a class. I may go as far as saying that you should never start writing a function or a class if you already don’t have stuff elsewhere in your code to immediately populate it with.

There are exceptions, of course. For some things it’s obvious right away that they should be extracted, even before any implementation is done.

In a broad sense, the main criterion is - a need for abstraction. Functions and classes are abstraction tools. They abstract away code and data and replace them with “placeholders” that facilitate hierarchical organization and reduce cognitive load of organizing a large system with many “moving parts”. So the question to always ask is - is this piece of functionality a candidate for abstraction at this time, and how much organizational power is gained (or lost) by abstracting it away.

In practice, just write dumb, unsophisticated code in the first available place. After the code is down and working properly, then look for good abstraction opportunities. Recognizing them takes practice and some theoretical knowledge of classical data structures, algorithms and design patterns as well as familiarity with the problem domain you’re trying to model. Don’t be afraid to make mistakes in how you abstract things away. The code can always be refactored, which is just another word for dividing the same code across a different set of abstractions.

Got it. That makes sense, thanks. Though to be fair…

…all of my code is dumb and unsophisticated at this point. :grin: